diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 52635d98efb..25630afe2bd 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -42,7 +42,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `awsAsyncQueryCaching` | Enable caching for async queries for Redshift and Athena. Requires that the datasource has caching and async query support enabled | Yes | | `angularDeprecationUI` | Display Angular warnings in dashboards and panels | Yes | | `dashgpt` | Enable AI powered features in dashboards | Yes | -| `libraryPanelRBAC` | Enables RBAC support for library panels | Yes | | `externalCorePlugins` | Allow core plugins to be loaded as external | Yes | | `panelMonitoring` | Enables panel monitoring through logs and measurements | Yes | | `formatString` | Enable format string transformer | Yes | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 9c76d39b592..e297e8d191d 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -265,11 +265,6 @@ export interface FeatureToggles { */ sseGroupByDatasource?: boolean; /** - * Enables RBAC support for library panels - * @default true - */ - libraryPanelRBAC?: boolean; - /** * Enables running Loki queries in parallel */ lokiRunQueriesInParallel?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index d59aacf4c7f..9faca508d2f 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -434,15 +434,6 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaObservabilityMetricsSquad, }, - { - Name: "libraryPanelRBAC", - Description: "Enables RBAC support for library panels", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: false, - Owner: grafanaDashboardsSquad, - RequiresRestart: true, - Expression: "true", - }, { Name: "lokiRunQueriesInParallel", Description: "Enables running Loki queries in parallel", diff --git a/pkg/services/featuremgmt/toggles-gitlog.csv b/pkg/services/featuremgmt/toggles-gitlog.csv index b7f4ab02da4..c3caa2aba8f 100644 --- a/pkg/services/featuremgmt/toggles-gitlog.csv +++ b/pkg/services/featuremgmt/toggles-gitlog.csv @@ -218,7 +218,6 @@ grafanaAPIServerWithExperimentalAPIs,2023-10-06T18:55:22Z,,717a9dd6160e352ff7c18 panelMonitoring,2023-10-09T05:19:08Z,,ef82767dabea7d19cd8efac0c36ed590d4d767f4,Victor Marin navAdminSubsections,2023-10-10T10:50:44Z,2023-11-17T10:04:34Z,f56cc6fdc01dbf2efb802f22650cb2bef0c40179,Ashley Harrison recoveryThreshold,2023-10-10T14:51:50Z,,810fbc3327841da6d21f945e75cad9daf040e625,Yuri Tseretyan -libraryPanelRBAC,2023-10-11T23:30:50Z,,a12cb8cbf3a9b33841b2f2cb1522be11de78c86a,kay delaney awsDatasourcesNewFormStyling,2023-10-12T08:59:10Z,2024-07-22T12:48:17Z,2771fb940342aa152377b26b9554eb15082f90ac,Ida Štambuk cachingOptimizeSerializationMemoryUsage,2023-10-12T16:56:49Z,,94ce87571ddfcede0fb7a229a65502b385d5bca3,Michael Mandrus panelTitleSearchInV1,2023-10-13T12:04:24Z,2025-01-21T09:59:32Z,bf2f2540da7a4e4b8d80e1fa4ae3d05868cf7b69,Arati R diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 876081e1830..478ed422a32 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -56,7 +56,6 @@ dashgpt,GA,@grafana/dashboards-squad,false,false,true aiGeneratedDashboardChanges,experimental,@grafana/dashboards-squad,false,false,true reportingRetries,preview,@grafana/grafana-operator-experience-squad,false,true,false sseGroupByDatasource,experimental,@grafana/observability-metrics,false,false,false -libraryPanelRBAC,GA,@grafana/dashboards-squad,false,true,false lokiRunQueriesInParallel,privatePreview,@grafana/observability-logs,false,false,false externalCorePlugins,GA,@grafana/plugins-platform-backend,false,false,false externalServiceAccounts,preview,@grafana/identity-access-team,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index be7e1439090..cf2dce3900a 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -235,10 +235,6 @@ const ( // Send query to the same datasource in a single request when using server side expressions. The `cloudWatchBatchQueries` feature toggle should be enabled if this used with CloudWatch. FlagSseGroupByDatasource = "sseGroupByDatasource" - // FlagLibraryPanelRBAC - // Enables RBAC support for library panels - FlagLibraryPanelRBAC = "libraryPanelRBAC" - // FlagLokiRunQueriesInParallel // Enables running Loki queries in parallel FlagLokiRunQueriesInParallel = "lokiRunQueriesInParallel" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index ebe38942f37..82b869e8a77 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1647,20 +1647,6 @@ "requiresRestart": true } }, - { - "metadata": { - "name": "libraryPanelRBAC", - "resourceVersion": "1750434297879", - "creationTimestamp": "2023-10-11T23:30:50Z" - }, - "spec": { - "description": "Enables RBAC support for library panels", - "stage": "GA", - "codeowner": "@grafana/dashboards-squad", - "requiresRestart": true, - "expression": "true" - } - }, { "metadata": { "name": "localeFormatPreference", diff --git a/pkg/services/libraryelements/api.go b/pkg/services/libraryelements/api.go index d74c81ef1db..21f705ac353 100644 --- a/pkg/services/libraryelements/api.go +++ b/pkg/services/libraryelements/api.go @@ -5,6 +5,7 @@ import ( "fmt" "hash/fnv" "net/http" + "strings" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" @@ -13,7 +14,6 @@ import ( ac "github.com/grafana/grafana/pkg/services/accesscontrol" 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/libraryelements/model" "github.com/grafana/grafana/pkg/services/org" @@ -25,24 +25,13 @@ func (l *LibraryElementService) registerAPIEndpoints() { l.RouteRegister.Group("/api/library-elements", func(entities routing.RouteRegister) { uidScope := ScopeLibraryPanelsProvider.GetResourceScopeUID(ac.Parameter(":uid")) - - if l.features.IsEnabledGlobally(featuremgmt.FlagLibraryPanelRBAC) { - entities.Post("/", authorize(ac.EvalPermission(ActionLibraryPanelsCreate)), routing.Wrap(l.createHandler)) - entities.Delete("/:uid", authorize(ac.EvalPermission(ActionLibraryPanelsDelete, uidScope)), routing.Wrap(l.deleteHandler)) - entities.Get("/", authorize(ac.EvalPermission(ActionLibraryPanelsRead)), routing.Wrap(l.getAllHandler)) - entities.Get("/:uid", authorize(ac.EvalPermission(ActionLibraryPanelsRead)), routing.Wrap(l.getHandler)) - entities.Get("/:uid/connections/", authorize(ac.EvalPermission(ActionLibraryPanelsRead, uidScope)), routing.Wrap(l.getConnectionsHandler)) - entities.Get("/name/:name", routing.Wrap(l.getByNameHandler)) - entities.Patch("/:uid", authorize(ac.EvalPermission(ActionLibraryPanelsWrite, uidScope)), routing.Wrap(l.patchHandler)) - } else { - entities.Post("/", routing.Wrap(l.createHandler)) - entities.Delete("/:uid", routing.Wrap(l.deleteHandler)) - entities.Get("/", routing.Wrap(l.getAllHandler)) - entities.Get("/:uid", routing.Wrap(l.getHandler)) - entities.Get("/:uid/connections/", routing.Wrap(l.getConnectionsHandler)) - entities.Get("/name/:name", routing.Wrap(l.getByNameHandler)) - entities.Patch("/:uid", routing.Wrap(l.patchHandler)) - } + entities.Post("/", authorize(ac.EvalPermission(ActionLibraryPanelsCreate)), routing.Wrap(l.createHandler)) + entities.Delete("/:uid", authorize(ac.EvalPermission(ActionLibraryPanelsDelete, uidScope)), routing.Wrap(l.deleteHandler)) + entities.Get("/", authorize(ac.EvalPermission(ActionLibraryPanelsRead)), routing.Wrap(l.getAllHandler)) + entities.Get("/:uid", authorize(ac.EvalPermission(ActionLibraryPanelsRead)), routing.Wrap(l.getHandler)) + entities.Get("/:uid/connections/", authorize(ac.EvalPermission(ActionLibraryPanelsRead, uidScope)), routing.Wrap(l.getConnectionsHandler)) + entities.Get("/name/:name", routing.Wrap(l.getByNameHandler)) + entities.Patch("/:uid", authorize(ac.EvalPermission(ActionLibraryPanelsWrite, uidScope)), routing.Wrap(l.patchHandler)) }) } @@ -155,13 +144,11 @@ func (l *LibraryElementService) getHandler(c *contextmodel.ReqContext) response. return l.toLibraryElementError(err, "Failed to get library element") } - if l.features.IsEnabled(ctx, featuremgmt.FlagLibraryPanelRBAC) { - allowed, err := l.AccessControl.Evaluate(ctx, c.SignedInUser, ac.EvalPermission(ActionLibraryPanelsRead, ScopeLibraryPanelsProvider.GetResourceScopeUID(web.Params(c.Req)[":uid"]))) - if err != nil { - return response.Error(http.StatusInternalServerError, "unable to evaluate library panel permissions", err) - } else if !allowed { - return response.Error(http.StatusForbidden, "insufficient permissions for getting library panel", err) - } + allowed, err := l.AccessControl.Evaluate(ctx, c.SignedInUser, ac.EvalPermission(ActionLibraryPanelsRead, ScopeLibraryPanelsProvider.GetResourceScopeUID(web.Params(c.Req)[":uid"]))) + if err != nil { + return response.Error(http.StatusInternalServerError, "unable to evaluate library panel permissions", err) + } else if !allowed { + return response.Error(http.StatusForbidden, "insufficient permissions for getting library panel", err) } return response.JSON(http.StatusOK, model.LibraryElementResponse{Result: element}) @@ -196,13 +183,11 @@ func (l *LibraryElementService) getAllHandler(c *contextmodel.ReqContext) respon return l.toLibraryElementError(err, "Failed to get library elements") } - if l.features.IsEnabled(c.Req.Context(), featuremgmt.FlagLibraryPanelRBAC) { - filteredPanels, err := l.filterLibraryPanelsByPermission(c, elementsResult.Elements) - if err != nil { - return l.toLibraryElementError(err, "Failed to evaluate permissions") - } - elementsResult.Elements = filteredPanels + filteredPanels, err := l.filterLibraryPanelsByPermission(c, elementsResult.Elements) + if err != nil { + return l.toLibraryElementError(err, "Failed to evaluate permissions") } + elementsResult.Elements = filteredPanels return response.JSON(http.StatusOK, model.LibraryElementSearchResponse{Result: elementsResult}) } @@ -235,6 +220,10 @@ func (l *LibraryElementService) patchHandler(c *contextmodel.ReqContext) respons } else { folder, err := l.folderService.Get(c.Req.Context(), &folder.GetFolderQuery{OrgID: c.GetOrgID(), UID: cmd.FolderUID, SignedInUser: c.SignedInUser}) if err != nil || folder == nil { + if errors.Is(err, dashboards.ErrFolderAccessDenied) { + return response.Error(http.StatusForbidden, "access denied to folder", err) + } + return response.Error(http.StatusBadRequest, "failed to get folder", err) } metrics.MFolderIDsServiceCount.WithLabelValues(metrics.LibraryElements).Inc() @@ -360,16 +349,12 @@ func (l *LibraryElementService) getByNameHandler(c *contextmodel.ReqContext) res return l.toLibraryElementError(err, "Failed to get library element") } - if l.features.IsEnabled(c.Req.Context(), featuremgmt.FlagLibraryPanelRBAC) { - filteredElements, err := l.filterLibraryPanelsByPermission(c, elements) - if err != nil { - return l.toLibraryElementError(err, err.Error()) - } - - return response.JSON(http.StatusOK, model.LibraryElementArrayResponse{Result: filteredElements}) - } else { - return response.JSON(http.StatusOK, model.LibraryElementArrayResponse{Result: elements}) + filteredElements, err := l.filterLibraryPanelsByPermission(c, elements) + if err != nil { + return l.toLibraryElementError(err, err.Error()) } + + return response.JSON(http.StatusOK, model.LibraryElementArrayResponse{Result: filteredElements}) } func (l *LibraryElementService) filterLibraryPanelsByPermission(c *contextmodel.ReqContext, elements []model.LibraryElementDTO) ([]model.LibraryElementDTO, error) { @@ -415,6 +400,10 @@ func (l *LibraryElementService) toLibraryElementError(err error, message string) if errors.Is(err, model.ErrLibraryElementUIDTooLong) { return response.Error(http.StatusBadRequest, model.ErrLibraryElementUIDTooLong.Error(), err) } + if err != nil && strings.Contains(err.Error(), "insufficient permissions") { + return response.Error(http.StatusForbidden, err.Error(), err) + } + // Log errors that cause internal server error status code. l.log.Error(message, "error", err) return response.ErrOrFallback(http.StatusInternalServerError, message, err) diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index 043d20d4713..ea91df4fe79 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -168,20 +168,12 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn } err = l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error { - if l.features.IsEnabled(c, featuremgmt.FlagLibraryPanelRBAC) { - allowed, err := l.AccessControl.Evaluate(c, signedInUser, ac.EvalPermission(ActionLibraryPanelsCreate, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID))) - if !allowed { - return fmt.Errorf("insufficient permissions for creating library panel in folder with UID %s", folderUID) - } - if err != nil { - return err - } - } else { - metrics.MFolderIDsServiceCount.WithLabelValues(metrics.LibraryElements).Inc() - // nolint:staticcheck - if err := l.requireEditPermissionsOnFolder(c, signedInUser, cmd.FolderID); err != nil { - return err - } + allowed, err := l.AccessControl.Evaluate(c, signedInUser, ac.EvalPermission(ActionLibraryPanelsCreate, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID))) + if !allowed { + return fmt.Errorf("insufficient permissions for creating library panel in folder with UID %s", folderUID) + } + if err != nil { + return err } if _, err := session.Insert(&element); err != nil { if l.SQLStore.GetDialect().IsUniqueConstraintViolation(err) { @@ -234,13 +226,6 @@ func (l *LibraryElementService) deleteLibraryElement(c context.Context, signedIn } metrics.MFolderIDsServiceCount.WithLabelValues(metrics.LibraryElements).Inc() - if !l.features.IsEnabled(c, featuremgmt.FlagLibraryPanelRBAC) { - // nolint:staticcheck - if err := l.requireEditPermissionsOnFolder(c, signedInUser, element.FolderID); err != nil { - return err - } - } - dashboardIDs := []int64{} // get all connections for this element if err := session.SQL("SELECT connection_id FROM library_element_connection where element_id = ?", element.ID).Find(&dashboardIDs); err != nil { @@ -581,20 +566,6 @@ func (l *LibraryElementService) handleFolderIDPatches(ctx context.Context, eleme toFolderID = fromFolderID } - if !l.features.IsEnabled(ctx, featuremgmt.FlagLibraryPanelRBAC) { - // FolderID was provided in the PATCH request - if toFolderID != -1 && toFolderID != fromFolderID { - if err := l.requireEditPermissionsOnFolder(ctx, user, toFolderID); err != nil { - return err - } - } - - // Always check permissions for the folder where library element resides - if err := l.requireEditPermissionsOnFolder(ctx, user, fromFolderID); err != nil { - return err - } - } - metrics.MFolderIDsServiceCount.WithLabelValues(metrics.LibraryElements).Inc() // nolint:staticcheck elementToPatch.FolderID = toFolderID diff --git a/pkg/services/libraryelements/guard.go b/pkg/services/libraryelements/guard.go index b746b1a998b..f372fe641ac 100644 --- a/pkg/services/libraryelements/guard.go +++ b/pkg/services/libraryelements/guard.go @@ -51,31 +51,6 @@ func (l *LibraryElementService) requireEditPermissionsOnFolderUID(ctx context.Co return nil } -func (l *LibraryElementService) requireEditPermissionsOnFolder(ctx context.Context, user identity.Requester, folderID int64) error { - // TODO remove these special cases and handle General folder case in access control guardian - if isGeneralFolder(folderID) && user.HasRole(org.RoleEditor) { - return nil - } - - if isGeneralFolder(folderID) && user.HasRole(org.RoleViewer) { - return dashboards.ErrFolderAccessDenied - } - - evaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, dashboards.ScopeFoldersProvider.GetResourceScope(strconv.FormatInt(folderID, 10))) - if isGeneralFolder(folderID) { - evaluator = accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.GeneralFolderUID)) - } - canEdit, err := l.AccessControl.Evaluate(ctx, user, evaluator) - if err != nil { - return err - } - if !canEdit { - return dashboards.ErrFolderAccessDenied - } - - return nil -} - func (l *LibraryElementService) requireViewPermissionsOnFolder(ctx context.Context, user identity.Requester, folderID int64) error { evaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersRead, dashboards.ScopeFoldersProvider.GetResourceScope(strconv.FormatInt(folderID, 10))) if isGeneralFolder(folderID) { diff --git a/pkg/services/libraryelements/libraryelements_get_all_test.go b/pkg/services/libraryelements/libraryelements_get_all_test.go index 8e69d7a2ee9..3c61d08b8ab 100644 --- a/pkg/services/libraryelements/libraryelements_get_all_test.go +++ b/pkg/services/libraryelements/libraryelements_get_all_test.go @@ -403,7 +403,7 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and folderFilterUIDs is set to existing folders, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { - newFolder := createFolder(t, sc, "NewFolder", nil) + newFolder := createFolder(t, sc, "NewFolder", sc.folderSvc) // nolint:staticcheck command := getCreatePanelCommand(newFolder.ID, newFolder.UID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) @@ -472,7 +472,7 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) { scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and folderFilter is set to a nonexistent folders, it should succeed and the result should be correct", func(t *testing.T, sc scenarioContext) { - newFolder := createFolder(t, sc, "NewFolder", nil) + newFolder := createFolder(t, sc, "NewFolder", sc.folderSvc) // nolint:staticcheck command := getCreatePanelCommand(newFolder.ID, sc.folder.UID, "Text - Library Panel2") sc.reqContext.Req.Body = mockRequestBody(command) diff --git a/pkg/services/libraryelements/libraryelements_get_test.go b/pkg/services/libraryelements/libraryelements_get_test.go index 42827e3f13a..b7d654167bc 100644 --- a/pkg/services/libraryelements/libraryelements_get_test.go +++ b/pkg/services/libraryelements/libraryelements_get_test.go @@ -99,7 +99,7 @@ func TestIntegration_GetLibraryElement(t *testing.T) { func(t *testing.T, sc scenarioContext) { b, err := json.Marshal(map[string]string{"test": "test"}) require.NoError(t, err) - newFolder := createFolder(t, sc, "NewFolder", nil) + newFolder := createFolder(t, sc, "NewFolder", sc.folderSvc) sc.reqContext.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersRead] = []string{dashboards.ScopeFoldersAll} sc.reqContext.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersDelete] = []string{dashboards.ScopeFoldersAll} result, err := sc.service.createLibraryElement(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, model.CreateLibraryElementCommand{ diff --git a/pkg/services/libraryelements/libraryelements_patch_test.go b/pkg/services/libraryelements/libraryelements_patch_test.go index 87c4bca1f56..f92076d041a 100644 --- a/pkg/services/libraryelements/libraryelements_patch_test.go +++ b/pkg/services/libraryelements/libraryelements_patch_test.go @@ -25,7 +25,7 @@ func TestIntegration_PatchLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to patch a library panel that exists, it should succeed", func(t *testing.T, sc scenarioContext) { - newFolder := createFolder(t, sc, "NewFolder", nil) + newFolder := createFolder(t, sc, "NewFolder", sc.folderSvc) cmd := model.PatchLibraryElementCommand{ FolderID: newFolder.ID, // nolint:staticcheck FolderUID: &newFolder.UID, @@ -92,7 +92,7 @@ func TestIntegration_PatchLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to patch a library panel with folder only, it should change folder successfully and return correct result", func(t *testing.T, sc scenarioContext) { - newFolder := createFolder(t, sc, "NewFolder", nil) + newFolder := createFolder(t, sc, "NewFolder", sc.folderSvc) cmd := model.PatchLibraryElementCommand{ FolderID: newFolder.ID, // nolint:staticcheck FolderUID: &newFolder.UID, @@ -337,7 +337,7 @@ func TestIntegration_PatchLibraryElement(t *testing.T) { scenarioWithPanel(t, "When an admin tries to patch a library panel with a folder where a library panel with the same name already exists, it should fail", func(t *testing.T, sc scenarioContext) { - newFolder := createFolder(t, sc, "NewFolder", nil) + newFolder := createFolder(t, sc, "NewFolder", sc.folderSvc) // nolint:staticcheck command := getCreatePanelCommand(newFolder.ID, newFolder.UID, "Text - Library Panel") sc.ctx.Req.Body = mockRequestBody(command) diff --git a/pkg/services/libraryelements/libraryelements_permissions_test.go b/pkg/services/libraryelements/libraryelements_permissions_test.go index fd682d70235..4f040b13fce 100644 --- a/pkg/services/libraryelements/libraryelements_permissions_test.go +++ b/pkg/services/libraryelements/libraryelements_permissions_test.go @@ -1,469 +1,372 @@ -package libraryelements +package libraryelements_test import ( + "bytes" + "context" "encoding/json" "fmt" + "io" "net/http" "testing" - "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" - "github.com/grafana/grafana/pkg/services/accesscontrol/actest" - "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/folder/folderimpl" + "github.com/grafana/grafana-openapi-client-go/models" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/libraryelements/model" "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/web" + "github.com/grafana/grafana/pkg/services/org/orgimpl" + "github.com/grafana/grafana/pkg/services/quota/quotaimpl" + "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/userimpl" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testinfra" ) -func TestLibraryElementPermissionsGeneralFolder(t *testing.T) { - testScenario(t, "When user with tries to create a library panel in the General folder, it should return correct status", - func(t *testing.T, sc scenarioContext) { - sc.reqContext.OrgRole = org.RoleViewer - command := getCreatePanelCommand(0, "", "Library Panel Name") - sc.reqContext.Req.Body = mockRequestBody(command) - resp := sc.service.createHandler(sc.reqContext) - require.Equal(t, http.StatusForbidden, resp.Status()) - - sc.reqContext.OrgRole = org.RoleEditor - sc.reqContext.Req.Body = mockRequestBody(command) - resp = sc.service.createHandler(sc.reqContext) - require.Equal(t, http.StatusOK, resp.Status()) +func TestIntegrationLibraryElementPermissions(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{}) + + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) + quotaService := quotaimpl.ProvideService(env.SQLStore, env.Cfg) + orgService, err := orgimpl.ProvideService(env.SQLStore, env.Cfg, quotaService) + require.NoError(t, err) + + sharedOrg, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "test org"}) + require.NoError(t, err) + + createUserInOrg(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleViewer), + Password: "viewer", + Login: "viewer", + OrgID: sharedOrg.ID, + }) + createUserInOrg(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleEditor), + Password: "editor", + Login: "editor", + OrgID: sharedOrg.ID, + }) + createUserInOrg(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Password: "admin", + Login: "admin2", + OrgID: sharedOrg.ID, + }) + + uid := "" + t.Run("create", func(t *testing.T) { + t.Run("When viewer tries to create a library panel in the General folder, it should fail", func(t *testing.T) { + createLibraryElement(t, grafanaListedAddr, "viewer", "viewer", "", http.StatusForbidden) + }) + + t.Run("When a user tries to create a library panel in a folder that doesn't exist, it should fail", func(t *testing.T) { + createLibraryElement(t, grafanaListedAddr, "admin2", "admin", "non-existent-folder-uid", http.StatusBadRequest) + }) + + t.Run("When editor tries to create a library panel in the General folder, it should succeed", func(t *testing.T) { + uid = createLibraryElement(t, grafanaListedAddr, "editor", "editor", "", http.StatusOK) + require.NotEmpty(t, uid) + require.NotEqual(t, uid, "") + }) + }) + + t.Run("move to folder", func(t *testing.T) { + folderUID := createTestFolder(t, grafanaListedAddr) + + t.Run("When viewer tries to move library panel to folder, it should fail", func(t *testing.T) { + patchLibraryElement(t, grafanaListedAddr, "viewer", "viewer", uid, folderUID, http.StatusForbidden) + }) + + t.Run("When a user tries to patch a library panel by moving it to a folder that doesn't exist, it should fail", func(t *testing.T) { + patchLibraryElement(t, grafanaListedAddr, "admin2", "admin", uid, "non-existent-folder-uid", http.StatusBadRequest) + }) + + t.Run("When editor tries to move library panel to folder, it should succeed", func(t *testing.T) { + patchLibraryElement(t, grafanaListedAddr, "editor", "editor", uid, folderUID, http.StatusOK) }) + }) - testScenario(t, "When user tries to patch a library panel by moving it to the General folder, it should return correct status", - func(t *testing.T, sc scenarioContext) { - folder := createFolder(t, sc, "Folder", nil) - // nolint:staticcheck - command := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel Name") - sc.reqContext.Req.Body = mockRequestBody(command) - resp := sc.service.createHandler(sc.reqContext) - result := validateAndUnMarshalResponse(t, resp) - - // nolint:staticcheck - sc.reqContext.OrgRole = org.RoleViewer - cmd := model.PatchLibraryElementCommand{FolderID: 0, Version: 1, Kind: int64(model.PanelElement)} - sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID}) - sc.ctx.Req.Body = mockRequestBody(cmd) - resp = sc.service.patchHandler(sc.reqContext) - require.Equal(t, http.StatusForbidden, resp.Status()) - - sc.reqContext.OrgRole = org.RoleEditor - sc.ctx.Req.Body = mockRequestBody(cmd) - resp = sc.service.patchHandler(sc.reqContext) - require.Equal(t, http.StatusOK, resp.Status()) + t.Run("move to general folder", func(t *testing.T) { + t.Run("When viewer tries to move library panel back to general, it should fail", func(t *testing.T) { + patchLibraryElement(t, grafanaListedAddr, "viewer", "viewer", uid, "", http.StatusForbidden) }) - testScenario(t, "When user tries to patch a library panel by moving it from the General folder, it should return correct status", - func(t *testing.T, sc scenarioContext) { - folder := createFolder(t, sc, "Folder", nil) - command := getCreatePanelCommand(0, "", "Library Panel Name") - sc.reqContext.Req.Body = mockRequestBody(command) - sc.service.AccessControl = actest.FakeAccessControl{ExpectedEvaluate: true} - resp := sc.service.createHandler(sc.reqContext) - result := validateAndUnMarshalResponse(t, resp) - - sc.reqContext.OrgRole = org.RoleViewer - cmd := model.PatchLibraryElementCommand{FolderUID: &folder.UID, Version: 1, Kind: int64(model.PanelElement)} - sc.service.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) - sc.service.AccessControl.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(folderimpl.ProvideDashboardFolderStore(sc.sqlStore), sc.service.folderService)) - sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID}) - sc.ctx.Req.Body = mockRequestBody(cmd) - resp = sc.service.patchHandler(sc.reqContext) - require.Equal(t, http.StatusForbidden, resp.Status()) - - sc.reqContext.OrgRole = org.RoleEditor - sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersWrite] = append(sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersWrite], dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.UID)) - sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersWrite] = append(sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersWrite], dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.GeneralFolderUID)) - sc.ctx.Req.Body = mockRequestBody(cmd) - resp = sc.service.patchHandler(sc.reqContext) - require.Equal(t, http.StatusOK, resp.Status()) + t.Run("When editor tries to move library panel back to general, it should succeed", func(t *testing.T) { + patchLibraryElement(t, grafanaListedAddr, "editor", "editor", uid, "", http.StatusOK) }) + }) - testScenario(t, "When user tries to delete a library panel in the General folder, it should return correct status", - func(t *testing.T, sc scenarioContext) { - cmd := getCreatePanelCommand(0, "", "Library Panel Name") - sc.reqContext.Req.Body = mockRequestBody(cmd) - sc.service.AccessControl = actest.FakeAccessControl{ExpectedEvaluate: true} - resp := sc.service.createHandler(sc.reqContext) - result := validateAndUnMarshalResponse(t, resp) - - sc.reqContext.OrgRole = org.RoleViewer - sc.service.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) - sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID}) - resp = sc.service.deleteHandler(sc.reqContext) - require.Equal(t, http.StatusForbidden, resp.Status()) - - sc.reqContext.OrgRole = org.RoleEditor - resp = sc.service.deleteHandler(sc.reqContext) - require.Equal(t, http.StatusOK, resp.Status()) + t.Run("get", func(t *testing.T) { + t.Run("When viewer tries to get library panel, it should succeed", func(t *testing.T) { + getLibraryElement(t, grafanaListedAddr, "viewer", "viewer", uid, http.StatusOK) }) - testScenario(t, "When user tries to get a library panel from General folder, it should return correct response", - func(t *testing.T, sc scenarioContext) { - sc.service.AccessControl = actest.FakeAccessControl{ExpectedEvaluate: true} - cmd := getCreatePanelCommand(0, "", "Library Panel in General Folder") - sc.reqContext.Req.Body = mockRequestBody(cmd) - resp := sc.service.createHandler(sc.reqContext) - result := validateAndUnMarshalResponse(t, resp) - result.Result.Meta.CreatedBy.Name = userInDbName - result.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar - result.Result.Meta.UpdatedBy.Name = userInDbName - result.Result.Meta.UpdatedBy.AvatarUrl = userInDbAvatar - result.Result.Meta.FolderName = "General" - result.Result.Meta.FolderUID = "general" - result.Result.FolderUID = "general" - - sc.service.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) - sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersRead] = append(sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersRead], dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.GeneralFolderUID)) - sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID}) - resp = sc.service.getHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - var actual libraryElementResult - err := json.Unmarshal(resp.Body(), &actual) - require.NoError(t, err) - if diff := cmp.Diff(result.Result, actual.Result, getCompareOptions()...); diff != "" { - t.Fatalf("Result mismatch (-want +got):\n%s", diff) - } + t.Run("When editor tries to get library panel, it should succeed", func(t *testing.T) { + getLibraryElement(t, grafanaListedAddr, "editor", "editor", uid, http.StatusOK) }) + }) - testScenario(t, "When user tries to get all library panels from General folder, it should return correct response", - func(t *testing.T, sc scenarioContext) { - sc.service.AccessControl = actest.FakeAccessControl{ExpectedEvaluate: true} - cmd := getCreatePanelCommand(0, "", "Library Panel in General Folder") - sc.reqContext.Req.Body = mockRequestBody(cmd) - resp := sc.service.createHandler(sc.reqContext) - result := validateAndUnMarshalResponse(t, resp) - result.Result.Meta.CreatedBy.Name = userInDbName - result.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar - result.Result.Meta.UpdatedBy.Name = userInDbName - result.Result.Meta.UpdatedBy.AvatarUrl = userInDbAvatar - result.Result.Meta.FolderName = "General" - - sc.service.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) - sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersRead] = append(sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersRead], dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.GeneralFolderUID)) - resp = sc.service.getAllHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - var actual libraryElementsSearch - err := json.Unmarshal(resp.Body(), &actual) - require.NoError(t, err) - require.Equal(t, 1, len(actual.Result.Elements)) - if diff := cmp.Diff(result.Result, actual.Result.Elements[0], getCompareOptions()...); diff != "" { - t.Fatalf("Result mismatch (-want +got):\n%s", diff) - } + t.Run("get all", func(t *testing.T) { + t.Run("When viewer tries to get all library elements, it should succeed", func(t *testing.T) { + getAllLibraryElements(t, grafanaListedAddr, "viewer", "viewer", http.StatusOK, 1) }) + + t.Run("When editor tries to get all library elements, it should succeed", func(t *testing.T) { + getAllLibraryElements(t, grafanaListedAddr, "editor", "editor", http.StatusOK, 1) + }) + }) + + t.Run("delete", func(t *testing.T) { + t.Run("When viewer tries to delete library panel, it should fail", func(t *testing.T) { + deleteLibraryElement(t, grafanaListedAddr, "viewer", "viewer", uid, http.StatusForbidden) + }) + + t.Run("When editor tries to delete library panel, it should succeed", func(t *testing.T) { + deleteLibraryElement(t, grafanaListedAddr, "editor", "editor", uid, http.StatusOK) + }) + }) } -func TestLibraryElementCreatePermissions(t *testing.T) { - var accessCases = []struct { - permissions map[string][]string - desc string - status int - }{ - { - desc: "can create library elements when granted write access to the correct folder", - permissions: map[string][]string{ - dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("uid_for_Folder")}, - dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, - }, - status: http.StatusOK, - }, - { - desc: "can create library elements when granted write access to all folders", - permissions: map[string][]string{ - dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, - dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, - }, - status: http.StatusOK, - }, - { - desc: "can't create library elements when granted write access to the wrong folder", - permissions: map[string][]string{ - dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("uid_for_Other_folder")}, - dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, - }, - status: http.StatusForbidden, - }, - { - desc: "can't create library elements when granted read access to the right folder", - permissions: map[string][]string{ - dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("uid_for_Folder")}, - }, - status: http.StatusForbidden, - }, +func TestIntegrationLibraryElementGranularPermissions(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") } + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{}) + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) + quotaService := quotaimpl.ProvideService(env.SQLStore, env.Cfg) + orgService, err := orgimpl.ProvideService(env.SQLStore, env.Cfg, quotaService) + require.NoError(t, err) + + sharedOrg, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "test org"}) + require.NoError(t, err) + + userID := createUserInOrg(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleViewer), + Password: "granular-viewer", + Login: "granular-viewer", + OrgID: sharedOrg.ID, + }) + createUserInOrg(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Password: "admin", + Login: "admin2", + OrgID: sharedOrg.ID, + }) + + folder1UID := createTestFolder(t, grafanaListedAddr) + folder2UID := createTestFolder(t, grafanaListedAddr) + folder3UID := createTestFolder(t, grafanaListedAddr) + + // viewer only has access to folder 1 & 3 + grantFolderPermissions(t, grafanaListedAddr, "granular-viewer", "granular-viewer", folder1UID, userID) + grantFolderPermissions(t, grafanaListedAddr, "granular-viewer", "granular-viewer", folder3UID, userID) + // revoke view access to folder2 + revokeFolderPermissions(t, grafanaListedAddr, folder2UID, userID) + + uid := "" + t.Run("granular createpermissions", func(t *testing.T) { + t.Run("When viewer has write access to folder1, they can create library element in folder1", func(t *testing.T) { + uid = createLibraryElement(t, grafanaListedAddr, "granular-viewer", "granular-viewer", folder1UID, http.StatusOK) + require.NotEmpty(t, uid) + }) - for _, testCase := range accessCases { - testScenario(t, testCase.desc, - func(t *testing.T, sc scenarioContext) { - folder := createFolder(t, sc, "Folder", nil) - sc.reqContext.Permissions = map[int64]map[string][]string{ - 1: testCase.permissions, - } - - // nolint:staticcheck - command := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel Name") - sc.reqContext.Req.Body = mockRequestBody(command) - resp := sc.service.createHandler(sc.reqContext) - require.Equal(t, testCase.status, resp.Status()) - }) - } + t.Run("When viewer doesn't have read access to folder2, they cannot create library element in folder2", func(t *testing.T) { + createLibraryElement(t, grafanaListedAddr, "granular-viewer", "granular-viewer", folder2UID, http.StatusBadRequest) + }) + + t.Run("When viewer doesn't have write access to general folder, they cannot create library element in general", func(t *testing.T) { + createLibraryElement(t, grafanaListedAddr, "granular-viewer", "granular-viewer", "", http.StatusForbidden) + }) + }) + + t.Run("granular move permissions", func(t *testing.T) { + t.Run("When viewer has write access to folder3 and folder1, they can move library element from folder1 to folder3", func(t *testing.T) { + patchLibraryElement(t, grafanaListedAddr, "granular-viewer", "granular-viewer", uid, folder3UID, http.StatusOK) + }) + + t.Run("When viewer doesn't have read access to folder2, they cannot move library element to folder2", func(t *testing.T) { + patchLibraryElement(t, grafanaListedAddr, "granular-viewer", "granular-viewer", uid, folder2UID, http.StatusBadRequest) + }) + }) + + inGeneralFolder := createLibraryElement(t, grafanaListedAddr, "admin2", "admin", "", http.StatusOK) + inFolder2 := createLibraryElement(t, grafanaListedAddr, "admin2", "admin", folder2UID, http.StatusOK) + + t.Run("granular read permissions", func(t *testing.T) { + t.Run("When viewer has read access to folder1, they can get library element from folder1", func(t *testing.T) { + getLibraryElement(t, grafanaListedAddr, "granular-viewer", "granular-viewer", uid, http.StatusOK) + }) + + t.Run("When viewer doesn't have read access to folder2, they cannot get library element from folder2", func(t *testing.T) { + getLibraryElement(t, grafanaListedAddr, "granular-viewer", "granular-viewer", inFolder2, http.StatusNotFound) + }) + + t.Run("When viewer has limited folder access, they only see library elements from accessible folders", func(t *testing.T) { + getAllLibraryElements(t, grafanaListedAddr, "granular-viewer", "granular-viewer", http.StatusOK, 2) + }) + }) + + t.Run("granular delete permissions", func(t *testing.T) { + t.Run("When viewer has write access to folder1, they can delete library element from folder1", func(t *testing.T) { + deleteLibraryElement(t, grafanaListedAddr, "granular-viewer", "granular-viewer", uid, http.StatusOK) + }) + + t.Run("When viewer doesn't have write access to folder2, they cannot delete library element from folder2", func(t *testing.T) { + deleteLibraryElement(t, grafanaListedAddr, "granular-viewer", "granular-viewer", inFolder2, http.StatusForbidden) + }) + + t.Run("When viewer doesn't have write access to general folder, they cannot delete library element from general", func(t *testing.T) { + deleteLibraryElement(t, grafanaListedAddr, "granular-viewer", "granular-viewer", inGeneralFolder, http.StatusForbidden) + }) + }) } -func TestLibraryElementPatchPermissions(t *testing.T) { - var accessCases = []struct { - permissions map[string][]string - desc string - status int - }{ - { - desc: "can move library elements when granted write access to the source and destination folders", - permissions: map[string][]string{ - dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("uid_for_FromFolder"), dashboards.ScopeFoldersProvider.GetResourceScopeUID("uid_for_ToFolder")}, - dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, - }, - status: http.StatusOK, - }, - { - desc: "can move library elements when granted write access to all folders", - permissions: map[string][]string{ - dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, - dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, - }, - status: http.StatusOK, - }, - { - desc: "can't move library elements when granted write access only to the source folder", - permissions: map[string][]string{ - dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("FromFolder")}, - dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, - }, - status: http.StatusForbidden, - }, - { - desc: "can't move library elements when granted write access to the destination folder", - permissions: map[string][]string{ - dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("ToFolder")}, - dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, - }, - status: http.StatusForbidden, - }, +/* + Helper functions +*/ + +func createLibraryElement(t *testing.T, grafanaListedAddr, user, password, folderUID string, expectedStatus int) string { + m := map[string]interface{}{ + "datasource": "${DS_GDEV-TESTDATA}", + "id": 1, + "title": "Text - Library Panel", + "type": "text", + "description": "A description", + } + createRequest := map[string]interface{}{ + "name": "Library Panel Name", + "model": m, + "folderUid": folderUID, + "kind": int64(1), } - for _, testCase := range accessCases { - testScenario(t, testCase.desc, - func(t *testing.T, sc scenarioContext) { - fromFolder := createFolder(t, sc, "FromFolder", nil) - // nolint:staticcheck - command := getCreatePanelCommand(fromFolder.ID, fromFolder.UID, "Library Panel Name") - sc.reqContext.Req.Body = mockRequestBody(command) - resp := sc.service.createHandler(sc.reqContext) - result := validateAndUnMarshalResponse(t, resp) - - toFolder := createFolder(t, sc, "ToFolder", nil) - - sc.reqContext.Permissions = map[int64]map[string][]string{ - 1: testCase.permissions, - } - - // nolint:staticcheck - cmd := model.PatchLibraryElementCommand{FolderID: toFolder.ID, FolderUID: &toFolder.UID, Version: 1, Kind: int64(model.PanelElement)} - sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID}) - sc.reqContext.Req.Body = mockRequestBody(cmd) - resp = sc.service.patchHandler(sc.reqContext) - require.Equal(t, testCase.status, resp.Status()) - }) + resp := makeHTTPRequest(t, "POST", fmt.Sprintf("http://%s:%s@%s/api/library-elements", user, password, grafanaListedAddr), createRequest, expectedStatus) + if expectedStatus == http.StatusOK { + var result model.LibraryElementResponse + err := json.Unmarshal(resp, &result) + require.NoError(t, err) + return result.Result.UID } + + return "" } -func TestLibraryElementDeletePermissions(t *testing.T) { - var accessCases = []struct { - permissions map[string][]string - desc string - status int - }{ - { - desc: "can delete library elements when granted write access to the correct folder", - permissions: map[string][]string{ - dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("uid_for_Folder")}, - dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("uid_for_Folder")}, - }, - status: http.StatusOK, - }, - { - desc: "can delete library elements when granted write access to all folders", - permissions: map[string][]string{ - dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, - dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, - }, - status: http.StatusOK, - }, - { - desc: "can't delete library elements when granted write access to the wrong folder", - permissions: map[string][]string{ - dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("Other_folder")}, - dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("Other_folder")}, - }, - status: http.StatusForbidden, - }, - { - desc: "can't delete library elements when granted read access to the right folder", - permissions: map[string][]string{ - dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("Folder")}, - }, - status: http.StatusForbidden, - }, +func patchLibraryElement(t *testing.T, grafanaListedAddr, user, password, uid, folderUID string, expectedStatus int) { + version := getLibraryElementVersion(t, grafanaListedAddr, user, password, uid) + patchRequest := map[string]interface{}{ + "folderUid": folderUID, + "version": version, + "kind": 1, } + makeHTTPRequest(t, "PATCH", fmt.Sprintf("http://%s:%s@%s/api/library-elements/%s", user, password, grafanaListedAddr, uid), patchRequest, expectedStatus) +} + +func deleteLibraryElement(t *testing.T, grafanaListedAddr, user, password, uid string, expectedStatus int) { + makeHTTPRequest(t, "DELETE", fmt.Sprintf("http://%s:%s@%s/api/library-elements/%s", user, password, grafanaListedAddr, uid), nil, expectedStatus) +} - for _, testCase := range accessCases { - testScenario(t, testCase.desc, - func(t *testing.T, sc scenarioContext) { - folder := createFolder(t, sc, "Folder", sc.service.folderService) - // nolint:staticcheck - command := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel Name") - sc.reqContext.Req.Body = mockRequestBody(command) - resp := sc.service.createHandler(sc.reqContext) - result := validateAndUnMarshalResponse(t, resp) - - sc.reqContext.Permissions = map[int64]map[string][]string{ - 1: testCase.permissions, - } - - sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID}) - resp = sc.service.deleteHandler(sc.reqContext) - require.Equal(t, testCase.status, resp.Status()) - }) +func getLibraryElement(t *testing.T, grafanaListedAddr, user, password, uid string, expectedStatus int) { + makeHTTPRequest(t, "GET", fmt.Sprintf("http://%s:%s@%s/api/library-elements/%s", user, password, grafanaListedAddr, uid), nil, expectedStatus) +} + +func getAllLibraryElements(t *testing.T, grafanaListedAddr, user, password string, expectedStatus int, expectedLength int) { + resp := makeHTTPRequest(t, "GET", fmt.Sprintf("http://%s:%s@%s/api/library-elements", user, password, grafanaListedAddr), nil, expectedStatus) + if expectedStatus == http.StatusOK { + var result model.LibraryElementSearchResponse + err := json.Unmarshal(resp, &result) + require.NoError(t, err) + require.Len(t, result.Result.Elements, expectedLength) } } -func TestLibraryElementsWithMissingFolders(t *testing.T) { - testScenario(t, "When a user tries to create a library panel in a folder that doesn't exist, it should fail", - func(t *testing.T, sc scenarioContext) { - command := getCreatePanelCommand(0, "badFolderUID", "Library Panel Name") - sc.reqContext.Req.Body = mockRequestBody(command) - resp := sc.service.createHandler(sc.reqContext) - fmt.Println(string(resp.Body())) - require.Equal(t, 400, resp.Status()) - }) +func getLibraryElementVersion(t *testing.T, grafanaListedAddr, user, password, uid string) int { + resp := makeHTTPRequest(t, "GET", fmt.Sprintf("http://%s:%s@%s/api/library-elements/%s", user, password, grafanaListedAddr, uid), nil, http.StatusOK) + var getResult model.LibraryElementResponse + err := json.Unmarshal(resp, &getResult) + require.NoError(t, err) - testScenario(t, "When a user tries to patch a library panel by moving it to a folder that doesn't exist, it should fail", - func(t *testing.T, sc scenarioContext) { - folder := createFolder(t, sc, "Folder", nil) - // nolint:staticcheck - command := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel Name") - sc.reqContext.Req.Body = mockRequestBody(command) - resp := sc.service.createHandler(sc.reqContext) - result := validateAndUnMarshalResponse(t, resp) - - folderUID := "badFolderUID" - // nolint:staticcheck - cmd := model.PatchLibraryElementCommand{FolderID: -100, FolderUID: &folderUID, Version: 1, Kind: int64(model.PanelElement)} - sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID}) - sc.reqContext.Req.Body = mockRequestBody(cmd) - resp = sc.service.patchHandler(sc.reqContext) - require.Equal(t, 400, resp.Status()) - }) + return int(getResult.Result.Version) } -func TestLibraryElementsGetPermissions(t *testing.T) { - var getCases = []struct { - permissions map[string][]string - desc string - status int - }{ - { - desc: "can get a library element when granted read access to all folders", - permissions: map[string][]string{ - dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, - }, - status: http.StatusOK, - }, - { - desc: "can't list library element when granted read access to the wrong folder", - permissions: map[string][]string{ - dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("Other_folder")}, - }, - status: http.StatusForbidden, - }, - } - for _, testCase := range getCases { - testScenario(t, testCase.desc, - func(t *testing.T, sc scenarioContext) { - folder := createFolder(t, sc, "Folder", nil) - // nolint:staticcheck - cmd := getCreatePanelCommand(folder.ID, folder.UID, "Library Panel") - sc.reqContext.Req.Body = mockRequestBody(cmd) - resp := sc.service.createHandler(sc.reqContext) - result := validateAndUnMarshalResponse(t, resp) - result.Result.Meta.CreatedBy.Name = userInDbName - result.Result.Meta.CreatedBy.AvatarUrl = userInDbAvatar - result.Result.Meta.UpdatedBy.Name = userInDbName - result.Result.Meta.UpdatedBy.AvatarUrl = userInDbAvatar - result.Result.Meta.FolderName = folder.Title - result.Result.Meta.FolderUID = folder.UID - - sc.reqContext.OrgRole = org.RoleViewer - sc.reqContext.Permissions = map[int64]map[string][]string{ - 1: testCase.permissions, - } - - sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID}) - resp = sc.service.getHandler(sc.reqContext) - require.Equal(t, testCase.status, resp.Status()) - }) +func createTestFolder(t *testing.T, grafanaListedAddr string) string { + folderRequest := map[string]interface{}{ + "title": "Test Folder", } + resp := makeHTTPRequest(t, "POST", fmt.Sprintf("http://admin2:admin@%s/api/folders", grafanaListedAddr), folderRequest, http.StatusOK) + var folder models.Folder + err := json.Unmarshal(resp, &folder) + require.NoError(t, err) + return folder.UID } -func TestLibraryElementsGetAllPermissions(t *testing.T) { - var getCases = []struct { - permissions map[string][]string - desc string - status int - expectedResultCount int - }{ - { - desc: "can get all library elements when granted read access to all folders", - permissions: map[string][]string{ - dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}, +func grantFolderPermissions(t *testing.T, grafanaListedAddr, user, password, folderUID string, userID int64) { + permissionRequest := map[string]interface{}{ + "items": []map[string]interface{}{ + { + "userId": userID, + "permission": 2, // edit permission }, - expectedResultCount: 2, - status: http.StatusOK, - }, - { - desc: "can't get any library element when doesn't have access to any folders", - permissions: map[string][]string{}, - expectedResultCount: 0, - status: http.StatusOK, }, } - for _, testCase := range getCases { - testScenario(t, testCase.desc, - func(t *testing.T, sc scenarioContext) { - for i := 1; i <= 2; i++ { - folder := createFolder(t, sc, fmt.Sprintf("Folder%d", i), nil) - // nolint:staticcheck - cmd := getCreatePanelCommand(folder.ID, folder.UID, fmt.Sprintf("Library Panel %d", i)) - sc.reqContext.Req.Body = mockRequestBody(cmd) - resp := sc.service.createHandler(sc.reqContext) - result := validateAndUnMarshalResponse(t, resp) - result.Result.Meta.FolderUID = folder.UID - } - - sc.reqContext.OrgRole = org.RoleViewer - sc.reqContext.Permissions = map[int64]map[string][]string{ - 1: testCase.permissions, - } - - resp := sc.service.getAllHandler(sc.reqContext) - require.Equal(t, 200, resp.Status()) - var actual libraryElementsSearch - err := json.Unmarshal(resp.Body(), &actual) - require.NoError(t, err) - require.Equal(t, testCase.expectedResultCount, len(actual.Result.Elements)) - }) + makeHTTPRequest(t, "POST", fmt.Sprintf("http://admin2:admin@%s/api/folders/%s/permissions", grafanaListedAddr, folderUID), permissionRequest, http.StatusOK) +} + +func revokeFolderPermissions(t *testing.T, grafanaListedAddr, folderUID string, userID int64) { + permissionRequest := map[string]interface{}{ + "items": []map[string]interface{}{}, + } + makeHTTPRequest(t, "POST", fmt.Sprintf("http://admin2:admin@%s/api/folders/%s/permissions", grafanaListedAddr, folderUID), permissionRequest, http.StatusOK) +} + +func makeHTTPRequest(t *testing.T, method, url string, body interface{}, expectedStatus int) []byte { + var req *http.Request + var err error + + if body != nil { + buf := &bytes.Buffer{} + err = json.NewEncoder(buf).Encode(body) + require.NoError(t, err) + req, err = http.NewRequest(method, url, buf) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + } else { + req, err = http.NewRequest(method, url, nil) + require.NoError(t, err) } + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + // nolint:errcheck + defer resp.Body.Close() + require.Equal(t, expectedStatus, resp.StatusCode) + + respBody, err := io.ReadAll(resp.Body) + require.NoError(t, err) + return respBody +} + +func createUserInOrg(t *testing.T, db db.DB, cfg *setting.Cfg, cmd user.CreateUserCommand) int64 { + t.Helper() + + cfg.AutoAssignOrg = true + cfg.AutoAssignOrgId = 1 + + quotaService := quotaimpl.ProvideService(db, cfg) + orgService, err := orgimpl.ProvideService(db, cfg, quotaService) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService( + db, orgService, cfg, nil, nil, tracing.InitializeTracerForTest(), + quotaService, supportbundlestest.NewFakeBundleService(), + ) + require.NoError(t, err) + + u, err := usrSvc.Create(context.Background(), &cmd) + require.NoError(t, err) + return u.ID } diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 238ab00a635..6feeec293d9 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -107,8 +107,9 @@ func TestIntegration_DeleteLibraryPanelsInFolder(t *testing.T) { func(t *testing.T, sc scenarioContext) { sc.service.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) sc.service.AccessControl.RegisterScopeAttributeResolver(dashboards.NewFolderUIDScopeResolver(sc.service.folderService)) - err := sc.service.DeleteLibraryElementsInFolder(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, sc.folder.UID+"xxxx") - require.ErrorIs(t, err, dashboards.ErrFolderAccessDenied) + sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.folder.UID + "xxxx"}) + resp := sc.service.deleteHandler(sc.reqContext) + require.Equal(t, http.StatusNotFound, resp.Status()) }) scenarioWithPanel(t, "When an admin tries to delete a folder that contains disconnected elements, it should delete all disconnected elements too", @@ -287,7 +288,7 @@ func TestIntegration_GetLibraryPanelConnections(t *testing.T) { func(t *testing.T, sc scenarioContext) { b, err := json.Marshal(map[string]string{"test": "test"}) require.NoError(t, err) - newFolder := createFolder(t, sc, "NewFolder", nil) + newFolder := createFolder(t, sc, "NewFolder", sc.folderSvc) sc.reqContext.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersRead] = []string{dashboards.ScopeFoldersAll} sc.reqContext.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersDelete] = []string{dashboards.ScopeFoldersAll} _, err = sc.service.createLibraryElement(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, model.CreateLibraryElementCommand{ @@ -386,6 +387,7 @@ type scenarioContext struct { initialResult libraryElementResult sqlStore db.DB log log.Logger + folderSvc folder.Service } func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash *dashboards.Dashboard, folderID int64, folderUID string) *dashboards.Dashboard { @@ -444,21 +446,6 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash func createFolder(t *testing.T, sc scenarioContext, title string, folderSvc folder.Service) *folder.Folder { t.Helper() - - if folderSvc == nil { - features := featuremgmt.WithFeatures() - cfg := setting.NewCfg() - ac := actest.FakeAccessControl{ExpectedEvaluate: true} - dashboardStore, err := database.ProvideDashboardStore(sc.sqlStore, cfg, features, tagimpl.ProvideService(sc.sqlStore)) - require.NoError(t, err) - - folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore) - store := folderimpl.ProvideStore(sc.sqlStore) - folderSvc = folderimpl.ProvideService( - store, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, - nil, sc.sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig) - t.Logf("Creating folder with title %q and UID uid_for_%s", title, title) - } ctx := identity.WithRequester(context.Background(), &sc.user) folder, err := folderSvc.Create(ctx, &folder.CreateFolderCommand{ OrgID: sc.user.OrgID, Title: title, UID: "uid_for_" + title, SignedInUser: &sc.user, @@ -505,33 +492,125 @@ func validateAndUnMarshalArrayResponse(t *testing.T, resp response.Response) lib return result } -func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) { +// setupTestScenario performs the common setup for library element tests +func setupTestScenario(t *testing.T) scenarioContext { t.Helper() + orgID := int64(1) + role := org.RoleAdmin + usr := user.SignedInUser{ + UserID: 1, + Name: "Signed In User", + Login: "signed_in_user", + Email: "signed.in.user@test.com", + OrgID: orgID, + OrgRole: role, + LastSeenAt: time.Now(), + // Allow user to create folders and library elements + Permissions: map[int64]map[string][]string{ + 1: { + dashboards.ActionFoldersCreate: {dashboards.ScopeFoldersAll}, + dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}, + dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}, + ActionLibraryPanelsCreate: {dashboards.ScopeFoldersAll}, + ActionLibraryPanelsRead: {ScopeLibraryPanelsAll}, + ActionLibraryPanelsWrite: {ScopeLibraryPanelsAll}, + ActionLibraryPanelsDelete: {ScopeLibraryPanelsAll}, + }, + }, + } + req := &http.Request{ + Header: http.Header{ + "Content-Type": []string{"application/json"}, + }, + } + ctx := identity.WithRequester(context.Background(), &usr) + req = req.WithContext(ctx) + webCtx := web.Context{Req: req} + features := featuremgmt.WithFeatures() + tracer := tracing.InitializeTracerForTest() sqlStore, cfg := db.InitTestDBWithCfg(t) - ac := actest.FakeAccessControl{} quotaService := quotatest.New(false, nil) dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore)) require.NoError(t, err) + ac := acimpl.ProvideAccessControl(features) folderPermissions := acmock.NewMockedPermissionsService() + folderPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) dashboardPermissions := acmock.NewMockedPermissionsService() folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) fStore := folderimpl.ProvideStore(sqlStore) + publicDash := &publicdashboards.FakePublicDashboardServiceWrapper{} + publicDash.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil) folderSvc := folderimpl.ProvideService( fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, - nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig) - dashboardService, svcErr := dashboardservice.ProvideDashboardServiceImpl( + nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), publicDash, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig) + alertStore, err := ngstore.ProvideDBStore(cfg, features, sqlStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest())) + require.NoError(t, err) + err = folderSvc.RegisterService(alertStore) + require.NoError(t, err) + dashService, dashSvcErr := dashboardservice.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, features, folderPermissions, ac, actest.FakeService{}, folderSvc, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(), serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), kvstore.NewFakeKVStore(), ) - require.NoError(t, svcErr) - dashboardService.RegisterDashboardPermissions(dashboardPermissions) + require.NoError(t, dashSvcErr) + dashService.RegisterDashboardPermissions(dashboardPermissions) + service := LibraryElementService{ + Cfg: cfg, + features: featuremgmt.WithFeatures(), + SQLStore: sqlStore, + folderService: folderSvc, + dashboardsService: dashService, + AccessControl: ac, + log: log.NewNopLogger(), + } + + service.AccessControl.RegisterScopeAttributeResolver(LibraryPanelUIDScopeResolver(&service, folderSvc)) + + // deliberate difference between signed in user and user in db to make it crystal clear + // what to expect in the tests + // In the real world these are identical + cmd := user.CreateUserCommand{ + Email: "user.in.db@test.com", + Name: "User In DB", + Login: userInDbName, + } + orgSvc, err := orgimpl.ProvideService(sqlStore, cfg, quotaService) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService( + sqlStore, orgSvc, cfg, nil, nil, tracer, + quotaService, supportbundlestest.NewFakeBundleService(), + ) + require.NoError(t, err) + _, err = usrSvc.Create(context.Background(), &cmd) + require.NoError(t, err) + + sc := scenarioContext{ + user: usr, + ctx: &webCtx, + service: &service, + sqlStore: sqlStore, + reqContext: &contextmodel.ReqContext{ + Context: &webCtx, + SignedInUser: &usr, + }, + folderSvc: folderSvc, + } + + sc.folder = createFolder(t, sc, "ScenarioFolder", folderSvc) + + return sc +} + +func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) { + t.Helper() + + t.Run(desc, func(t *testing.T) { + sc := setupTestScenario(t) - testScenario(t, desc, func(t *testing.T, sc scenarioContext) { // nolint:staticcheck command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Text - Library Panel") sc.reqContext.Req.Body = mockRequestBody(command) @@ -549,100 +628,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo t.Helper() t.Run(desc, func(t *testing.T) { - orgID := int64(1) - role := org.RoleAdmin - usr := user.SignedInUser{ - UserID: 1, - Name: "Signed In User", - Login: "signed_in_user", - Email: "signed.in.user@test.com", - OrgID: orgID, - OrgRole: role, - LastSeenAt: time.Now(), - // Allow user to create folders - Permissions: map[int64]map[string][]string{ - 1: {dashboards.ActionFoldersCreate: {dashboards.ScopeFoldersAll}}, - }, - } - req := &http.Request{ - Header: http.Header{ - "Content-Type": []string{"application/json"}, - }, - } - ctx := identity.WithRequester(context.Background(), &usr) - req = req.WithContext(ctx) - webCtx := web.Context{Req: req} - - features := featuremgmt.WithFeatures() - tracer := tracing.InitializeTracerForTest() - sqlStore, cfg := db.InitTestDBWithCfg(t) - quotaService := quotatest.New(false, nil) - dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore)) - require.NoError(t, err) - ac := acimpl.ProvideAccessControl(features) - folderPermissions := acmock.NewMockedPermissionsService() - folderPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) - dashboardPermissions := acmock.NewMockedPermissionsService() - folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - fStore := folderimpl.ProvideStore(sqlStore) - publicDash := &publicdashboards.FakePublicDashboardServiceWrapper{} - publicDash.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil) - folderSvc := folderimpl.ProvideService( - fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, - nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), publicDash, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig) - alertStore, err := ngstore.ProvideDBStore(cfg, features, sqlStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest())) - require.NoError(t, err) - err = folderSvc.RegisterService(alertStore) - require.NoError(t, err) - dashService, dashSvcErr := dashboardservice.ProvideDashboardServiceImpl( - cfg, dashboardStore, folderStore, - features, folderPermissions, ac, actest.FakeService{}, folderSvc, - nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(), - serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()), - kvstore.NewFakeKVStore(), - ) - require.NoError(t, dashSvcErr) - dashService.RegisterDashboardPermissions(dashboardPermissions) - service := LibraryElementService{ - Cfg: cfg, - features: featuremgmt.WithFeatures(), - SQLStore: sqlStore, - folderService: folderSvc, - dashboardsService: dashService, - AccessControl: ac, - log: log.NewNopLogger(), - } - - // deliberate difference between signed in user and user in db to make it crystal clear - // what to expect in the tests - // In the real world these are identical - cmd := user.CreateUserCommand{ - Email: "user.in.db@test.com", - Name: "User In DB", - Login: userInDbName, - } - orgSvc, err := orgimpl.ProvideService(sqlStore, cfg, quotaService) - require.NoError(t, err) - usrSvc, err := userimpl.ProvideService( - sqlStore, orgSvc, cfg, nil, nil, tracer, - quotaService, supportbundlestest.NewFakeBundleService(), - ) - require.NoError(t, err) - _, err = usrSvc.Create(context.Background(), &cmd) - require.NoError(t, err) - - sc := scenarioContext{ - user: usr, - ctx: &webCtx, - service: &service, - sqlStore: sqlStore, - reqContext: &contextmodel.ReqContext{ - Context: &webCtx, - SignedInUser: &usr, - }, - } - - sc.folder = createFolder(t, sc, "ScenarioFolder", folderSvc) + sc := setupTestScenario(t) fn(t, sc) })