Library panels: Remove libraryPanelRBAC feature flag, and enable rbac by default (#107222)
This commit is contained in:
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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())
|
||||
func TestIntegrationLibraryElementPermissions(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{})
|
||||
|
||||
sc.reqContext.OrgRole = org.RoleEditor
|
||||
sc.reqContext.Req.Body = mockRequestBody(command)
|
||||
resp = sc.service.createHandler(sc.reqContext)
|
||||
require.Equal(t, http.StatusOK, resp.Status())
|
||||
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)
|
||||
})
|
||||
|
||||
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("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)
|
||||
})
|
||||
|
||||
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)
|
||||
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, "")
|
||||
})
|
||||
})
|
||||
|
||||
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())
|
||||
t.Run("move to folder", func(t *testing.T) {
|
||||
folderUID := createTestFolder(t, grafanaListedAddr)
|
||||
|
||||
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 viewer tries to move library panel to folder, it should fail", func(t *testing.T) {
|
||||
patchLibraryElement(t, grafanaListedAddr, "viewer", "viewer", uid, folderUID, http.StatusForbidden)
|
||||
})
|
||||
|
||||
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("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)
|
||||
})
|
||||
|
||||
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"
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
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("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 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("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)
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
t.Run("When editor tries to get library panel, it should succeed", func(t *testing.T) {
|
||||
getLibraryElement(t, grafanaListedAddr, "editor", "editor", uid, http.StatusOK)
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
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) {
|
||||
folder := createFolder(t, sc, "Folder", nil)
|
||||
sc.reqContext.Permissions = map[int64]map[string][]string{
|
||||
1: testCase.permissions,
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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())
|
||||
})
|
||||
return ""
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 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,
|
||||
},
|
||||
}
|
||||
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)
|
||||
|
||||
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())
|
||||
})
|
||||
}
|
||||
return int(getResult.Result.Version)
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
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 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 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())
|
||||
})
|
||||
|
||||
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())
|
||||
})
|
||||
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
|
||||
},
|
||||
},
|
||||
}
|
||||
makeHTTPRequest(t, "POST", fmt.Sprintf("http://admin2:admin@%s/api/folders/%s/permissions", grafanaListedAddr, folderUID), permissionRequest, http.StatusOK)
|
||||
}
|
||||
|
||||
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 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 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()},
|
||||
},
|
||||
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
|
||||
}
|
||||
func makeHTTPRequest(t *testing.T, method, url string, body interface{}, expectedStatus int) []byte {
|
||||
var req *http.Request
|
||||
var err error
|
||||
|
||||
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))
|
||||
})
|
||||
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)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user