Library panels: Remove `libraryPanelRBAC` feature flag, and enable rbac by default (#107222)

pull/107291/head
Stephanie Hingtgen 3 weeks ago committed by GitHub
parent 3cd29d2cdd
commit bc231922af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
  2. 5
      packages/grafana-data/src/types/featureToggles.gen.ts
  3. 9
      pkg/services/featuremgmt/registry.go
  4. 1
      pkg/services/featuremgmt/toggles-gitlog.csv
  5. 1
      pkg/services/featuremgmt/toggles_gen.csv
  6. 4
      pkg/services/featuremgmt/toggles_gen.go
  7. 14
      pkg/services/featuremgmt/toggles_gen.json
  8. 71
      pkg/services/libraryelements/api.go
  9. 41
      pkg/services/libraryelements/database.go
  10. 25
      pkg/services/libraryelements/guard.go
  11. 4
      pkg/services/libraryelements/libraryelements_get_all_test.go
  12. 2
      pkg/services/libraryelements/libraryelements_get_test.go
  13. 6
      pkg/services/libraryelements/libraryelements_patch_test.go
  14. 739
      pkg/services/libraryelements/libraryelements_permissions_test.go
  15. 224
      pkg/services/libraryelements/libraryelements_test.go

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

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

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

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

1 #name created deleted hash author
218 panelMonitoring 2023-10-09T05:19:08Z ef82767dabea7d19cd8efac0c36ed590d4d767f4 Victor Marin
219 navAdminSubsections 2023-10-10T10:50:44Z 2023-11-17T10:04:34Z f56cc6fdc01dbf2efb802f22650cb2bef0c40179 Ashley Harrison
220 recoveryThreshold 2023-10-10T14:51:50Z 810fbc3327841da6d21f945e75cad9daf040e625 Yuri Tseretyan
libraryPanelRBAC 2023-10-11T23:30:50Z a12cb8cbf3a9b33841b2f2cb1522be11de78c86a kay delaney
221 awsDatasourcesNewFormStyling 2023-10-12T08:59:10Z 2024-07-22T12:48:17Z 2771fb940342aa152377b26b9554eb15082f90ac Ida Štambuk
222 cachingOptimizeSerializationMemoryUsage 2023-10-12T16:56:49Z 94ce87571ddfcede0fb7a229a65502b385d5bca3 Michael Mandrus
223 panelTitleSearchInV1 2023-10-13T12:04:24Z 2025-01-21T09:59:32Z bf2f2540da7a4e4b8d80e1fa4ae3d05868cf7b69 Arati R

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

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
56 aiGeneratedDashboardChanges experimental @grafana/dashboards-squad false false true
57 reportingRetries preview @grafana/grafana-operator-experience-squad false true false
58 sseGroupByDatasource experimental @grafana/observability-metrics false false false
libraryPanelRBAC GA @grafana/dashboards-squad false true false
59 lokiRunQueriesInParallel privatePreview @grafana/observability-logs false false false
60 externalCorePlugins GA @grafana/plugins-platform-backend false false false
61 externalServiceAccounts preview @grafana/identity-access-team false false false

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

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

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

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

@ -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) {

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

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

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

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

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

Loading…
Cancel
Save