PluginManager: Make Plugins, Renderer and DataSources non-global (#31866)

* PluginManager: Make Plugins and DataSources non-global

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Fix integration tests

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Replace outdated command

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* DashboardService: Ensure it gets constructed with necessary parameters

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Fix build

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* DashboardService: Ensure it gets constructed with necessary parameters

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Remove dead code

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Fix test

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Fix test

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Remove FocusConvey

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Fix test

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Remove dead code

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Undo interface changes

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Backend: Move tsdbifaces.RequestHandler to plugins.DataRequestHandler

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Rename to DataSourceCount

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Consolidate dashboard interfaces into one

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Fix tests

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Fix tests

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Fix test

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Fix tests

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Fix tests

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Fix tests

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Fix tests

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Fix dashboard integration tests

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
This commit is contained in:
Arve Knudsen
2021-03-17 16:06:10 +01:00
committed by GitHub
parent f1df32ac03
commit 87c3a2b790
95 changed files with 2455 additions and 2426 deletions
+2 -8
View File
@@ -3,11 +3,10 @@ package dashboards
import (
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
)
func MakeUserAdmin(bus bus.Bus, orgID int64, userID int64, dashboardID int64, setViewAndEditPermissions bool) error {
func (dr *dashboardServiceImpl) MakeUserAdmin(orgID int64, userID int64, dashboardID int64, setViewAndEditPermissions bool) error {
rtEditor := models.ROLE_EDITOR
rtViewer := models.ROLE_VIEWER
@@ -43,12 +42,7 @@ func MakeUserAdmin(bus bus.Bus, orgID int64, userID int64, dashboardID int64, se
)
}
aclCmd := &models.UpdateDashboardAclCommand{
DashboardID: dashboardID,
Items: items,
}
if err := bus.Dispatch(aclCmd); err != nil {
if err := dr.dashboardStore.UpdateDashboardACL(dashboardID, items); err != nil {
return err
}
+75 -74
View File
@@ -1,10 +1,13 @@
package dashboards
import (
"fmt"
"strings"
"time"
"github.com/grafana/grafana/pkg/components/gtime"
"github.com/grafana/grafana/pkg/dashboards"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/bus"
@@ -20,6 +23,7 @@ type DashboardService interface {
SaveDashboard(dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error)
ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error)
DeleteDashboard(dashboardId int64, orgId int64) error
MakeUserAdmin(orgID int64, userID, dashboardID int64, setViewAndEditPermissions bool) error
}
// DashboardProvisioningService is a service for operating on provisioned dashboards.
@@ -27,21 +31,22 @@ type DashboardProvisioningService interface {
SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
SaveFolderForProvisionedDashboards(*SaveDashboardDTO) (*models.Dashboard, error)
GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
GetProvisionedDashboardDataByDashboardID(DashboardId int64) (*models.DashboardProvisioning, error)
UnprovisionDashboard(dashboardId int64) error
DeleteProvisionedDashboard(dashboardId int64, orgId int64) error
GetProvisionedDashboardDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error)
UnprovisionDashboard(dashboardID int64) error
DeleteProvisionedDashboard(dashboardID int64, orgID int64) error
}
// NewService is a factory for creating a new dashboard service.
var NewService = func() DashboardService {
var NewService = func(store dashboards.Store) DashboardService {
return &dashboardServiceImpl{
log: log.New("dashboard-service"),
dashboardStore: store,
log: log.New("dashboard-service"),
}
}
// NewProvisioningService is a factory for creating a new dashboard provisioning service.
var NewProvisioningService = func() DashboardProvisioningService {
return NewService().(*dashboardServiceImpl)
var NewProvisioningService = func(store dashboards.Store) DashboardProvisioningService {
return NewService(store).(*dashboardServiceImpl)
}
type SaveDashboardDTO struct {
@@ -54,35 +59,32 @@ type SaveDashboardDTO struct {
}
type dashboardServiceImpl struct {
orgId int64
user *models.SignedInUser
log log.Logger
dashboardStore dashboards.Store
orgId int64
user *models.SignedInUser
log log.Logger
}
func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
cmd := &models.GetProvisionedDashboardDataQuery{Name: name}
err := bus.Dispatch(cmd)
if err != nil {
return nil, err
}
return cmd.Result, nil
return dr.dashboardStore.GetProvisionedDashboardData(name)
}
func (dr *dashboardServiceImpl) GetProvisionedDashboardDataByDashboardID(dashboardId int64) (*models.DashboardProvisioning, error) {
cmd := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: dashboardId}
err := bus.Dispatch(cmd)
if err != nil {
return nil, err
}
return cmd.Result, nil
// GetProvisionedData gets provisioned dashboard data.
//
// Stubbable by tests.
var GetProvisionedData = func(store dashboards.Store, dashboardID int64) (*models.DashboardProvisioning, error) {
return store.GetProvisionedDataByDashboardID(dashboardID)
}
func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlerts bool,
func (dr *dashboardServiceImpl) GetProvisionedDashboardDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) {
return GetProvisionedData(dr.dashboardStore, dashboardID)
}
func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, shouldValidateAlerts bool,
validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) {
dash := dto.Dashboard
dash.OrgId = dto.OrgId
dash.Title = strings.TrimSpace(dash.Title)
dash.Data.Set("title", dash.Title)
dash.SetUid(strings.TrimSpace(dash.Uid))
@@ -109,29 +111,18 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
return nil, err
}
if validateAlerts {
validateAlertsCmd := models.ValidateDashboardAlertsCommand{
OrgId: dto.OrgId,
Dashboard: dash,
User: dto.User,
}
if err := bus.Dispatch(&validateAlertsCmd); err != nil {
if shouldValidateAlerts {
if err := validateAlerts(dash, dto.User); err != nil {
return nil, err
}
}
validateBeforeSaveCmd := models.ValidateDashboardBeforeSaveCommand{
OrgId: dto.OrgId,
Dashboard: dash,
Overwrite: dto.Overwrite,
}
if err := bus.Dispatch(&validateBeforeSaveCmd); err != nil {
isParentFolderChanged, err := dr.dashboardStore.ValidateDashboardBeforeSave(dash, dto.Overwrite)
if err != nil {
return nil, err
}
if validateBeforeSaveCmd.Result.IsParentFolderChanged {
if isParentFolderChanged {
folderGuardian := guardian.New(dash.FolderId, dto.OrgId, dto.User)
if canSave, err := folderGuardian.CanSave(); err != nil || !canSave {
if err != nil {
@@ -178,6 +169,11 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
return cmd, nil
}
var validateAlerts = func(dash *models.Dashboard, user *models.SignedInUser) error {
extractor := alerting.NewDashAlertExtractor(dash, dash.OrgId, user)
return extractor.ValidateAlerts()
}
func validateDashboardRefreshInterval(dash *models.Dashboard) error {
if setting.MinRefreshInterval == "" {
return nil
@@ -191,11 +187,11 @@ func validateDashboardRefreshInterval(dash *models.Dashboard) error {
minRefreshInterval, err := gtime.ParseDuration(setting.MinRefreshInterval)
if err != nil {
return err
return fmt.Errorf("parsing min refresh interval %q failed: %w", setting.MinRefreshInterval, err)
}
d, err := gtime.ParseDuration(refresh)
if err != nil {
return err
return fmt.Errorf("parsing refresh duration %q failed: %w", refresh, err)
}
if d < minRefreshInterval {
@@ -205,14 +201,17 @@ func validateDashboardRefreshInterval(dash *models.Dashboard) error {
return nil
}
func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand, dto *SaveDashboardDTO) error {
alertCmd := models.UpdateDashboardAlertsCommand{
OrgId: dto.OrgId,
Dashboard: cmd.Result,
User: dto.User,
// UpdateAlerting updates alerting.
//
// Stubbable by tests.
var UpdateAlerting = func(store dashboards.Store, orgID int64, dashboard *models.Dashboard, user *models.SignedInUser) error {
extractor := alerting.NewDashAlertExtractor(dashboard, orgID, user)
alerts, err := extractor.GetAlerts()
if err != nil {
return err
}
return bus.Dispatch(&alertCmd)
return store.SaveAlerts(dashboard.Id, alerts)
}
func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO,
@@ -234,24 +233,18 @@ func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO,
return nil, err
}
saveCmd := &models.SaveProvisionedDashboardCommand{
DashboardCmd: cmd,
DashboardProvisioning: provisioning,
}
// dashboard
err = bus.Dispatch(saveCmd)
dash, err := dr.dashboardStore.SaveProvisionedDashboard(*cmd, provisioning)
if err != nil {
return nil, err
}
// alerts
err = dr.updateAlerting(cmd, dto)
if err != nil {
if err := UpdateAlerting(dr.dashboardStore, dto.OrgId, dash, dto.User); err != nil {
return nil, err
}
return cmd.Result, nil
return dash, nil
}
func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDashboardDTO) (*models.Dashboard, error) {
@@ -264,22 +257,23 @@ func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDash
return nil, err
}
err = bus.Dispatch(cmd)
dash, err := dr.dashboardStore.SaveDashboard(*cmd)
if err != nil {
return nil, err
}
err = dr.updateAlerting(cmd, dto)
if err != nil {
if err := UpdateAlerting(dr.dashboardStore, dto.OrgId, dash, dto.User); err != nil {
return nil, err
}
return cmd.Result, nil
return dash, nil
}
func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error) {
if err := validateDashboardRefreshInterval(dto.Dashboard); err != nil {
dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval", "dashboardUid", dto.Dashboard.Uid, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", setting.MinRefreshInterval)
dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval",
"dashboardUid", dto.Dashboard.Uid, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval",
setting.MinRefreshInterval)
dto.Dashboard.Data.Set("refresh", setting.MinRefreshInterval)
}
@@ -288,17 +282,16 @@ func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO, allowUiUpda
return nil, err
}
err = bus.Dispatch(cmd)
dash, err := dr.dashboardStore.SaveDashboard(*cmd)
if err != nil {
return nil, fmt.Errorf("saving dashboard failed: %w", err)
}
if err := UpdateAlerting(dr.dashboardStore, dto.OrgId, dash, dto.User); err != nil {
return nil, err
}
err = dr.updateAlerting(cmd, dto)
if err != nil {
return nil, err
}
return cmd.Result, nil
return dash, nil
}
// DeleteDashboard removes dashboard from the DB. Errors out if the dashboard was provisioned. Should be used for
@@ -327,9 +320,12 @@ func (dr *dashboardServiceImpl) deleteDashboard(dashboardId int64, orgId int64,
return bus.Dispatch(cmd)
}
func (dr *dashboardServiceImpl) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
func (dr *dashboardServiceImpl) ImportDashboard(dto *SaveDashboardDTO) (
*models.Dashboard, error) {
if err := validateDashboardRefreshInterval(dto.Dashboard); err != nil {
dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval", "dashboardUid", dto.Dashboard.Uid, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", setting.MinRefreshInterval)
dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval",
"dashboardUid", dto.Dashboard.Uid, "dashboardTitle", dto.Dashboard.Title,
"minRefreshInterval", setting.MinRefreshInterval)
dto.Dashboard.Data.Set("refresh", setting.MinRefreshInterval)
}
@@ -359,6 +355,7 @@ type FakeDashboardService struct {
SaveDashboardResult *models.Dashboard
SaveDashboardError error
SavedDashboards []*SaveDashboardDTO
ProvisionedDashData *models.DashboardProvisioning
}
func (s *FakeDashboardService) SaveDashboard(dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error) {
@@ -385,8 +382,12 @@ func (s *FakeDashboardService) DeleteDashboard(dashboardId int64, orgId int64) e
return nil
}
func (s *FakeDashboardService) GetProvisionedDashboardDataByDashboardID(id int64) (*models.DashboardProvisioning, error) {
return s.ProvisionedDashData, nil
}
func MockDashboardService(mock *FakeDashboardService) {
NewService = func() DashboardService {
NewService = func(dashboards.Store) DashboardService {
return mock
}
}
@@ -0,0 +1,928 @@
// +build integration
package dashboards
import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/dashboards"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/models"
)
const testOrgID int64 = 1
func TestIntegratedDashboardService(t *testing.T) {
t.Run("Given saved folders and dashboards in organization A", func(t *testing.T) {
origUpdateAlerting := UpdateAlerting
t.Cleanup(func() {
UpdateAlerting = origUpdateAlerting
})
UpdateAlerting = func(store dashboards.Store, orgID int64, dashboard *models.Dashboard, user *models.SignedInUser) error {
return nil
}
// Basic validation tests
permissionScenario(t, "When saving a dashboard with non-existing id", true,
func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: testOrgID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": float64(123412321),
"title": "Expect error",
}),
}
err := callSaveWithError(cmd, sc.sqlStore)
assert.Equal(t, models.ErrDashboardNotFound, err)
})
// Given other organization
t.Run("Given organization B", func(t *testing.T) {
const otherOrgId int64 = 2
permissionScenario(t, "When creating a dashboard with same id as dashboard in organization A",
true, func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: otherOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": sc.savedDashInFolder.Id,
"title": "Expect error",
}),
Overwrite: false,
}
err := callSaveWithError(cmd, sc.sqlStore)
assert.Equal(t, models.ErrDashboardNotFound, err)
})
permissionScenario(t, "When creating a dashboard with same uid as dashboard in organization A, it should create a new dashboard in org B",
true, func(t *testing.T, sc *permissionScenarioContext) {
const otherOrgId int64 = 2
cmd := models.SaveDashboardCommand{
OrgId: otherOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": sc.savedDashInFolder.Uid,
"title": "Dash with existing uid in other org",
}),
Overwrite: false,
}
res := callSaveWithResult(t, cmd, sc.sqlStore)
require.NotNil(t, res)
dash, err := sc.sqlStore.GetDashboard(0, otherOrgId, sc.savedDashInFolder.Uid, "")
require.NoError(t, err)
assert.NotEqual(t, sc.savedDashInFolder.Id, dash.Id)
assert.Equal(t, res.Id, dash.Id)
assert.Equal(t, otherOrgId, dash.OrgId)
assert.Equal(t, sc.savedDashInFolder.Uid, dash.Uid)
})
})
t.Run("Given user has no permission to save", func(t *testing.T) {
const canSave = false
permissionScenario(t, "When creating a new dashboard in the General folder", canSave,
func(t *testing.T, sc *permissionScenarioContext) {
sqlStore := sqlstore.InitTestDB(t)
cmd := models.SaveDashboardCommand{
OrgId: testOrgID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Dash",
}),
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd, sqlStore)
assert.Equal(t, models.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, int64(0), sc.dashboardGuardianMock.DashId)
assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId)
assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId)
})
permissionScenario(t, "When creating a new dashboard in other folder, it should create dashboard guardian for other folder with correct arguments and rsult in access denied error",
canSave, func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: testOrgID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Dash",
}),
FolderId: sc.otherSavedFolder.Id,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd, sc.sqlStore)
require.Equal(t, models.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.otherSavedFolder.Id, sc.dashboardGuardianMock.DashId)
assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId)
assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId)
})
permissionScenario(t, "When creating a new dashboard by existing title in folder, it should create dashboard guardian for folder with correct arguments and result in access denied error",
canSave, func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: testOrgID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": sc.savedDashInFolder.Title,
}),
FolderId: sc.savedFolder.Id,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd, sc.sqlStore)
require.Equal(t, models.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.savedFolder.Id, sc.dashboardGuardianMock.DashId)
assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId)
assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId)
})
permissionScenario(t, "When creating a new dashboard by existing UID in folder, it should create dashboard guardian for folder with correct arguments and result in access denied error",
canSave, func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: testOrgID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": sc.savedDashInFolder.Uid,
"title": "New dash",
}),
FolderId: sc.savedFolder.Id,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd, sc.sqlStore)
require.Equal(t, models.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.savedFolder.Id, sc.dashboardGuardianMock.DashId)
assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId)
assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId)
})
permissionScenario(t, "When updating a dashboard by existing id in the General folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error",
canSave, func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: testOrgID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": sc.savedDashInGeneralFolder.Id,
"title": "Dash",
}),
FolderId: sc.savedDashInGeneralFolder.FolderId,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd, sc.sqlStore)
assert.Equal(t, models.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.savedDashInGeneralFolder.Id, sc.dashboardGuardianMock.DashId)
assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId)
assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId)
})
permissionScenario(t, "When updating a dashboard by existing id in other folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error",
canSave, func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: testOrgID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": sc.savedDashInFolder.Id,
"title": "Dash",
}),
FolderId: sc.savedDashInFolder.FolderId,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd, sc.sqlStore)
require.Equal(t, models.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.savedDashInFolder.Id, sc.dashboardGuardianMock.DashId)
assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId)
assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId)
})
permissionScenario(t, "When moving a dashboard by existing ID to other folder from General folder, it should create dashboard guardian for other folder with correct arguments and result in access denied error",
canSave, func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: testOrgID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": sc.savedDashInGeneralFolder.Id,
"title": "Dash",
}),
FolderId: sc.otherSavedFolder.Id,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd, sc.sqlStore)
require.Equal(t, models.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.otherSavedFolder.Id, sc.dashboardGuardianMock.DashId)
assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId)
assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId)
})
permissionScenario(t, "When moving a dashboard by existing id to the General folder from other folder, it should create dashboard guardian for General folder with correct arguments and result in access denied error",
canSave, func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: testOrgID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": sc.savedDashInFolder.Id,
"title": "Dash",
}),
FolderId: 0,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd, sc.sqlStore)
assert.Equal(t, models.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, int64(0), sc.dashboardGuardianMock.DashId)
assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId)
assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId)
})
permissionScenario(t, "When moving a dashboard by existing uid to other folder from General folder, it should create dashboard guardian for other folder with correct arguments and result in access denied error",
canSave, func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: testOrgID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": sc.savedDashInGeneralFolder.Uid,
"title": "Dash",
}),
FolderId: sc.otherSavedFolder.Id,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd, sc.sqlStore)
require.Equal(t, models.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.otherSavedFolder.Id, sc.dashboardGuardianMock.DashId)
assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId)
assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId)
})
permissionScenario(t, "When moving a dashboard by existing UID to the General folder from other folder, it should create dashboard guardian for General folder with correct arguments and result in access denied error",
canSave, func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: testOrgID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": sc.savedDashInFolder.Uid,
"title": "Dash",
}),
FolderId: 0,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd, sc.sqlStore)
require.Equal(t, models.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, int64(0), sc.dashboardGuardianMock.DashId)
assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId)
assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId)
})
})
t.Run("Given user has permission to save", func(t *testing.T) {
const canSave = true
t.Run("and overwrite flag is set to false", func(t *testing.T) {
const shouldOverwrite = false
permissionScenario(t, "When creating a dashboard in General folder with same name as dashboard in other folder",
canSave, func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: testOrgID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": sc.savedDashInFolder.Title,
}),
FolderId: 0,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(t, cmd, sc.sqlStore)
require.NotNil(t, res)
dash, err := sc.sqlStore.GetDashboard(res.Id, cmd.OrgId, "", "")
require.NoError(t, err)
assert.Equal(t, res.Id, dash.Id)
assert.Equal(t, int64(0), dash.FolderId)
})
permissionScenario(t, "When creating a dashboard in other folder with same name as dashboard in General folder",
canSave, func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: testOrgID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": sc.savedDashInGeneralFolder.Title,
}),
FolderId: sc.savedFolder.Id,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(t, cmd, sc.sqlStore)
require.NotNil(t, res)
assert.NotEqual(t, sc.savedDashInGeneralFolder.Id, res.Id)
dash, err := sc.sqlStore.GetDashboard(res.Id, cmd.OrgId, "", "")
require.NoError(t, err)
assert.Equal(t, sc.savedFolder.Id, dash.FolderId)
})
permissionScenario(t, "When creating a folder with same name as dashboard in other folder",
canSave, func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: testOrgID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": sc.savedDashInFolder.Title,
}),
IsFolder: true,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(t, cmd, sc.sqlStore)
require.NotNil(t, res)
assert.NotEqual(t, sc.savedDashInGeneralFolder.Id, res.Id)
assert.True(t, res.IsFolder)
dash, err := sc.sqlStore.GetDashboard(res.Id, cmd.OrgId, "", "")
require.NoError(t, err)
assert.Equal(t, int64(0), dash.FolderId)
assert.True(t, dash.IsFolder)
})
permissionScenario(t, "When saving a dashboard without id and uid and unique title in folder",
canSave, func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: testOrgID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Dash without id and uid",
}),
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(t, cmd, sc.sqlStore)
require.NotNil(t, res)
assert.Greater(t, res.Id, int64(0))
assert.NotEmpty(t, res.Uid)
dash, err := sc.sqlStore.GetDashboard(res.Id, cmd.OrgId, "", "")
require.NoError(t, err)
assert.Equal(t, res.Id, dash.Id)
assert.Equal(t, res.Uid, dash.Uid)
})
permissionScenario(t, "When saving a dashboard when dashboard id is zero ", canSave,
func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: testOrgID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": 0,
"title": "Dash with zero id",
}),
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(t, cmd, sc.sqlStore)
require.NotNil(t, res)
dash, err := sc.sqlStore.GetDashboard(res.Id, cmd.OrgId, "", "")
require.NoError(t, err)
assert.Equal(t, res.Id, dash.Id)
})
permissionScenario(t, "When saving a dashboard in non-existing folder", canSave,
func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: testOrgID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Expect error",
}),
FolderId: 123412321,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
assert.Equal(t, models.ErrDashboardFolderNotFound, err)
})
permissionScenario(t, "When updating an existing dashboard by id without current version", canSave,
func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": sc.savedDashInGeneralFolder.Id,
"title": "test dash 23",
}),
FolderId: sc.savedFolder.Id,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
assert.Equal(t, models.ErrDashboardVersionMismatch, err)
})
permissionScenario(t, "When updating an existing dashboard by id with current version", canSave,
func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": sc.savedDashInGeneralFolder.Id,
"title": "Updated title",
"version": sc.savedDashInGeneralFolder.Version,
}),
FolderId: sc.savedFolder.Id,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(t, cmd, sc.sqlStore)
require.NotNil(t, res)
dash, err := sc.sqlStore.GetDashboard(sc.savedDashInGeneralFolder.Id, cmd.OrgId, "", "")
require.NoError(t, err)
assert.Equal(t, "Updated title", dash.Title)
assert.Equal(t, sc.savedFolder.Id, dash.FolderId)
assert.Greater(t, dash.Version, sc.savedDashInGeneralFolder.Version)
})
permissionScenario(t, "When updating an existing dashboard by uid without current version", canSave,
func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": sc.savedDashInFolder.Uid,
"title": "test dash 23",
}),
FolderId: 0,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
assert.Equal(t, models.ErrDashboardVersionMismatch, err)
})
permissionScenario(t, "When updating an existing dashboard by uid with current version", canSave,
func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": sc.savedDashInFolder.Uid,
"title": "Updated title",
"version": sc.savedDashInFolder.Version,
}),
FolderId: 0,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(t, cmd, sc.sqlStore)
require.NotNil(t, res)
dash, err := sc.sqlStore.GetDashboard(sc.savedDashInFolder.Id, cmd.OrgId, "", "")
require.NoError(t, err)
assert.Equal(t, "Updated title", dash.Title)
assert.Equal(t, int64(0), dash.FolderId)
assert.Greater(t, dash.Version, sc.savedDashInFolder.Version)
})
permissionScenario(t, "When creating a dashboard with same name as dashboard in other folder",
canSave, func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: testOrgID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": sc.savedDashInFolder.Title,
}),
FolderId: sc.savedDashInFolder.FolderId,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
assert.Equal(t, models.ErrDashboardWithSameNameInFolderExists, err)
})
permissionScenario(t, "When creating a dashboard with same name as dashboard in General folder",
canSave, func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: testOrgID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": sc.savedDashInGeneralFolder.Title,
}),
FolderId: sc.savedDashInGeneralFolder.FolderId,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
assert.Equal(t, models.ErrDashboardWithSameNameInFolderExists, err)
})
permissionScenario(t, "When creating a folder with same name as existing folder", canSave,
func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: testOrgID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": sc.savedFolder.Title,
}),
IsFolder: true,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
assert.Equal(t, models.ErrDashboardWithSameNameInFolderExists, err)
})
})
t.Run("and overwrite flag is set to true", func(t *testing.T) {
const shouldOverwrite = true
permissionScenario(t, "When updating an existing dashboard by id without current version", canSave,
func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": sc.savedDashInGeneralFolder.Id,
"title": "Updated title",
}),
FolderId: sc.savedFolder.Id,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(t, cmd, sc.sqlStore)
require.NotNil(t, res)
dash, err := sc.sqlStore.GetDashboard(sc.savedDashInGeneralFolder.Id, cmd.OrgId, "", "")
require.NoError(t, err)
assert.Equal(t, "Updated title", dash.Title)
assert.Equal(t, sc.savedFolder.Id, dash.FolderId)
assert.Greater(t, dash.Version, sc.savedDashInGeneralFolder.Version)
})
permissionScenario(t, "When updating an existing dashboard by uid without current version", canSave,
func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": sc.savedDashInFolder.Uid,
"title": "Updated title",
}),
FolderId: 0,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(t, cmd, sc.sqlStore)
require.NotNil(t, res)
dash, err := sc.sqlStore.GetDashboard(sc.savedDashInFolder.Id, cmd.OrgId, "", "")
require.NoError(t, err)
assert.Equal(t, "Updated title", dash.Title)
assert.Equal(t, int64(0), dash.FolderId)
assert.Greater(t, dash.Version, sc.savedDashInFolder.Version)
})
permissionScenario(t, "When updating uid for existing dashboard using id", canSave,
func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": sc.savedDashInFolder.Id,
"uid": "new-uid",
"title": sc.savedDashInFolder.Title,
}),
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(t, cmd, sc.sqlStore)
require.NotNil(t, res)
assert.Equal(t, sc.savedDashInFolder.Id, res.Id)
assert.Equal(t, "new-uid", res.Uid)
dash, err := sc.sqlStore.GetDashboard(sc.savedDashInFolder.Id, cmd.OrgId, "", "")
require.NoError(t, err)
assert.Equal(t, "new-uid", dash.Uid)
assert.Greater(t, dash.Version, sc.savedDashInFolder.Version)
})
permissionScenario(t, "When updating uid to an existing uid for existing dashboard using id", canSave,
func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": sc.savedDashInFolder.Id,
"uid": sc.savedDashInGeneralFolder.Uid,
"title": sc.savedDashInFolder.Title,
}),
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
assert.Equal(t, models.ErrDashboardWithSameUIDExists, err)
})
permissionScenario(t, "When creating a dashboard with same name as dashboard in other folder", canSave,
func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: testOrgID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": sc.savedDashInFolder.Title,
}),
FolderId: sc.savedDashInFolder.FolderId,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(t, cmd, sc.sqlStore)
require.NotNil(t, res)
assert.Equal(t, sc.savedDashInFolder.Id, res.Id)
assert.Equal(t, sc.savedDashInFolder.Uid, res.Uid)
dash, err := sc.sqlStore.GetDashboard(res.Id, cmd.OrgId, "", "")
require.NoError(t, err)
assert.Equal(t, res.Id, dash.Id)
assert.Equal(t, res.Uid, dash.Uid)
})
permissionScenario(t, "When creating a dashboard with same name as dashboard in General folder", canSave,
func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: testOrgID,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": sc.savedDashInGeneralFolder.Title,
}),
FolderId: sc.savedDashInGeneralFolder.FolderId,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(t, cmd, sc.sqlStore)
require.NotNil(t, res)
assert.Equal(t, sc.savedDashInGeneralFolder.Id, res.Id)
assert.Equal(t, sc.savedDashInGeneralFolder.Uid, res.Uid)
dash, err := sc.sqlStore.GetDashboard(res.Id, cmd.OrgId, "", "")
require.NoError(t, err)
assert.Equal(t, res.Id, dash.Id)
assert.Equal(t, res.Uid, dash.Uid)
})
permissionScenario(t, "When updating existing folder to a dashboard using id", canSave,
func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": sc.savedFolder.Id,
"title": "new title",
}),
IsFolder: false,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
assert.Equal(t, models.ErrDashboardTypeMismatch, err)
})
permissionScenario(t, "When updating existing dashboard to a folder using id", canSave,
func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": sc.savedDashInFolder.Id,
"title": "new folder title",
}),
IsFolder: true,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
assert.Equal(t, models.ErrDashboardTypeMismatch, err)
})
permissionScenario(t, "When updating existing folder to a dashboard using uid", canSave,
func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": sc.savedFolder.Uid,
"title": "new title",
}),
IsFolder: false,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
assert.Equal(t, models.ErrDashboardTypeMismatch, err)
})
permissionScenario(t, "When updating existing dashboard to a folder using uid", canSave,
func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": sc.savedDashInFolder.Uid,
"title": "new folder title",
}),
IsFolder: true,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
assert.Equal(t, models.ErrDashboardTypeMismatch, err)
})
permissionScenario(t, "When updating existing folder to a dashboard using title", canSave,
func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": sc.savedFolder.Title,
}),
IsFolder: false,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
assert.Equal(t, models.ErrDashboardWithSameNameAsFolder, err)
})
permissionScenario(t, "When updating existing dashboard to a folder using title", canSave,
func(t *testing.T, sc *permissionScenarioContext) {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": sc.savedDashInGeneralFolder.Title,
}),
IsFolder: true,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
assert.Equal(t, models.ErrDashboardFolderWithSameNameAsDashboard, err)
})
})
})
})
}
type permissionScenarioContext struct {
dashboardGuardianMock *guardian.FakeDashboardGuardian
sqlStore *sqlstore.SQLStore
savedFolder *models.Dashboard
savedDashInFolder *models.Dashboard
otherSavedFolder *models.Dashboard
savedDashInGeneralFolder *models.Dashboard
}
type permissionScenarioFunc func(t *testing.T, sc *permissionScenarioContext)
func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionScenarioFunc) {
t.Helper()
mock := &guardian.FakeDashboardGuardian{
CanSaveValue: canSave,
}
t.Run(desc, func(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
savedFolder := saveTestFolder(t, "Saved folder", testOrgID, sqlStore)
savedDashInFolder := saveTestDashboard(t, "Saved dash in folder", testOrgID, savedFolder.Id, sqlStore)
saveTestDashboard(t, "Other saved dash in folder", testOrgID, savedFolder.Id, sqlStore)
savedDashInGeneralFolder := saveTestDashboard(t, "Saved dashboard in general folder", testOrgID, 0, sqlStore)
otherSavedFolder := saveTestFolder(t, "Other saved folder", testOrgID, sqlStore)
require.Equal(t, "Saved folder", savedFolder.Title)
require.Equal(t, "saved-folder", savedFolder.Slug)
require.NotEqual(t, int64(0), savedFolder.Id)
require.True(t, savedFolder.IsFolder)
require.Equal(t, int64(0), savedFolder.FolderId)
require.NotEmpty(t, savedFolder.Uid)
require.Equal(t, "Saved dash in folder", savedDashInFolder.Title)
require.Equal(t, "saved-dash-in-folder", savedDashInFolder.Slug)
require.NotEqual(t, int64(0), savedDashInFolder.Id)
require.False(t, savedDashInFolder.IsFolder)
require.Equal(t, savedFolder.Id, savedDashInFolder.FolderId)
require.NotEmpty(t, savedDashInFolder.Uid)
origNewDashboardGuardian := guardian.New
t.Cleanup(func() {
guardian.New = origNewDashboardGuardian
})
guardian.MockDashboardGuardian(mock)
sc := &permissionScenarioContext{
dashboardGuardianMock: mock,
sqlStore: sqlStore,
savedDashInFolder: savedDashInFolder,
otherSavedFolder: otherSavedFolder,
savedDashInGeneralFolder: savedDashInGeneralFolder,
savedFolder: savedFolder,
}
fn(t, sc)
})
}
func callSaveWithResult(t *testing.T, cmd models.SaveDashboardCommand, sqlStore *sqlstore.SQLStore) *models.Dashboard {
t.Helper()
dto := toSaveDashboardDto(cmd)
res, err := NewService(sqlStore).SaveDashboard(&dto, false)
require.NoError(t, err)
return res
}
func callSaveWithError(cmd models.SaveDashboardCommand, sqlStore *sqlstore.SQLStore) error {
dto := toSaveDashboardDto(cmd)
_, err := NewService(sqlStore).SaveDashboard(&dto, false)
return err
}
func saveTestDashboard(t *testing.T, title string, orgID, folderID int64, sqlStore *sqlstore.SQLStore) *models.Dashboard {
t.Helper()
cmd := models.SaveDashboardCommand{
OrgId: orgID,
FolderId: folderID,
IsFolder: false,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": title,
}),
}
dto := SaveDashboardDTO{
OrgId: orgID,
Dashboard: cmd.GetDashboardModel(),
User: &models.SignedInUser{
UserId: 1,
OrgRole: models.ROLE_ADMIN,
},
}
res, err := NewService(sqlStore).SaveDashboard(&dto, false)
require.NoError(t, err)
return res
}
func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore *sqlstore.SQLStore) *models.Dashboard {
t.Helper()
cmd := models.SaveDashboardCommand{
OrgId: orgID,
FolderId: 0,
IsFolder: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": title,
}),
}
dto := SaveDashboardDTO{
OrgId: orgID,
Dashboard: cmd.GetDashboardModel(),
User: &models.SignedInUser{
UserId: 1,
OrgRole: models.ROLE_ADMIN,
},
}
res, err := NewService(sqlStore).SaveDashboard(&dto, false)
require.NoError(t, err)
return res
}
func toSaveDashboardDto(cmd models.SaveDashboardCommand) SaveDashboardDTO {
dash := (&cmd).GetDashboardModel()
return SaveDashboardDTO{
Dashboard: dash,
Message: cmd.Message,
OrgId: cmd.OrgId,
User: &models.SignedInUser{UserId: cmd.UserId},
Overwrite: cmd.Overwrite,
}
}
+108 -74
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"testing"
"github.com/grafana/grafana/pkg/dashboards"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/setting"
@@ -17,8 +18,10 @@ func TestDashboardService(t *testing.T) {
Convey("Dashboard service tests", t, func() {
bus.ClearBusHandlers()
fakeStore := fakeDashboardStore{}
service := &dashboardServiceImpl{
log: log.New("test.logger"),
log: log.New("test.logger"),
dashboardStore: &fakeStore,
}
origNewDashboardGuardian := guardian.New
@@ -51,19 +54,13 @@ func TestDashboardService(t *testing.T) {
})
Convey("When saving a dashboard should validate uid", func() {
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
origValidateAlerts := validateAlerts
t.Cleanup(func() {
validateAlerts = origValidateAlerts
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
validateAlerts = func(dash *models.Dashboard, user *models.SignedInUser) error {
return nil
})
bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error {
cmd.Result = nil
return nil
})
}
testCases := []struct {
Uid string
@@ -89,27 +86,23 @@ func TestDashboardService(t *testing.T) {
})
Convey("Should return validation error if dashboard is provisioned", func() {
provisioningValidated := false
bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error {
provisioningValidated = true
cmd.Result = &models.DashboardProvisioning{}
return nil
t.Cleanup(func() {
fakeStore.provisionedData = nil
})
fakeStore.provisionedData = &models.DashboardProvisioning{}
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
origValidateAlerts := validateAlerts
t.Cleanup(func() {
validateAlerts = origValidateAlerts
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
validateAlerts = func(dash *models.Dashboard, user *models.SignedInUser) error {
return nil
})
}
dto.Dashboard = models.NewDashboard("Dash")
dto.Dashboard.SetId(3)
dto.User = &models.SignedInUser{UserId: 1}
_, err := service.SaveDashboard(dto, false)
So(provisioningValidated, ShouldBeTrue)
So(err, ShouldEqual, models.ErrDashboardCannotSaveProvisionedDashboard)
})
@@ -121,21 +114,20 @@ func TestDashboardService(t *testing.T) {
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
origValidateAlerts := validateAlerts
t.Cleanup(func() {
validateAlerts = origValidateAlerts
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
validateAlerts = func(dash *models.Dashboard, user *models.SignedInUser) error {
return nil
})
}
dto.Dashboard = models.NewDashboard("Dash")
dto.Dashboard.SetId(3)
dto.User = &models.SignedInUser{UserId: 1}
_, err := service.SaveDashboard(dto, true)
So(provisioningValidated, ShouldBeFalse)
So(err, ShouldNotBeNil)
So(err, ShouldBeNil)
})
Convey("Should return validation error if alert data is invalid", func() {
@@ -144,9 +136,13 @@ func TestDashboardService(t *testing.T) {
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return fmt.Errorf("alert validation error")
origValidateAlerts := validateAlerts
t.Cleanup(func() {
validateAlerts = origValidateAlerts
})
validateAlerts = func(dash *models.Dashboard, user *models.SignedInUser) error {
return fmt.Errorf("alert validation error")
}
dto.Dashboard = models.NewDashboard("Dash")
_, err := service.SaveDashboard(dto, false)
@@ -165,23 +161,27 @@ func TestDashboardService(t *testing.T) {
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
origUpdateAlerting := UpdateAlerting
t.Cleanup(func() {
UpdateAlerting = origUpdateAlerting
})
UpdateAlerting = func(store dashboards.Store, orgID int64, dashboard *models.Dashboard,
user *models.SignedInUser) error {
return nil
}
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
return nil
origValidateAlerts := validateAlerts
t.Cleanup(func() {
validateAlerts = origValidateAlerts
})
validateAlerts = func(dash *models.Dashboard, user *models.SignedInUser) error {
return nil
}
bus.AddHandler("test", func(cmd *models.SaveProvisionedDashboardCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error {
return nil
})
dto.Dashboard = models.NewDashboard("Dash")
dto.Dashboard.SetId(3)
dto.User = &models.SignedInUser{UserId: 1}
@@ -200,22 +200,26 @@ func TestDashboardService(t *testing.T) {
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
origValidateAlerts := validateAlerts
t.Cleanup(func() {
validateAlerts = origValidateAlerts
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
validateAlerts = func(dash *models.Dashboard, user *models.SignedInUser) error {
return nil
})
}
bus.AddHandler("test", func(cmd *models.SaveProvisionedDashboardCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error {
return nil
origUpdateAlerting := UpdateAlerting
t.Cleanup(func() {
UpdateAlerting = origUpdateAlerting
})
UpdateAlerting = func(store dashboards.Store, orgID int64, dashboard *models.Dashboard,
user *models.SignedInUser) error {
return nil
}
dto.Dashboard = models.NewDashboard("Dash")
dto.Dashboard.SetId(3)
@@ -231,41 +235,42 @@ func TestDashboardService(t *testing.T) {
dto := &SaveDashboardDTO{}
Convey("Should return validation error if dashboard is provisioned", func() {
provisioningValidated := false
bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error {
provisioningValidated = true
cmd.Result = &models.DashboardProvisioning{}
return nil
t.Cleanup(func() {
fakeStore.provisionedData = nil
})
fakeStore.provisionedData = &models.DashboardProvisioning{}
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
origValidateAlerts := validateAlerts
t.Cleanup(func() {
validateAlerts = origValidateAlerts
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
validateAlerts = func(dash *models.Dashboard, user *models.SignedInUser) error {
return nil
})
}
bus.AddHandler("test", func(cmd *models.SaveProvisionedDashboardCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error {
return nil
origUpdateAlerting := UpdateAlerting
t.Cleanup(func() {
UpdateAlerting = origUpdateAlerting
})
UpdateAlerting = func(store dashboards.Store, orgID int64, dashboard *models.Dashboard,
user *models.SignedInUser) error {
return nil
}
dto.Dashboard = models.NewDashboard("Dash")
dto.Dashboard.SetId(3)
dto.User = &models.SignedInUser{UserId: 1}
_, err := service.ImportDashboard(dto)
So(provisioningValidated, ShouldBeTrue)
So(err, ShouldEqual, models.ErrDashboardCannotSaveProvisionedDashboard)
})
})
Convey("Given provisioned dashboard", func() {
result := setupDeleteHandlers(true)
result := setupDeleteHandlers(t, &fakeStore, true)
Convey("DeleteProvisionedDashboard should delete it", func() {
err := service.DeleteProvisionedDashboard(1, 1)
@@ -281,7 +286,7 @@ func TestDashboardService(t *testing.T) {
})
Convey("Given non provisioned dashboard", func() {
result := setupDeleteHandlers(false)
result := setupDeleteHandlers(t, &fakeStore, false)
Convey("DeleteProvisionedDashboard should delete it", func() {
err := service.DeleteProvisionedDashboard(1, 1)
@@ -306,15 +311,15 @@ type Result struct {
deleteWasCalled bool
}
func setupDeleteHandlers(provisioned bool) *Result {
bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error {
if provisioned {
cmd.Result = &models.DashboardProvisioning{}
} else {
cmd.Result = nil
}
return nil
func setupDeleteHandlers(t *testing.T, fakeStore *fakeDashboardStore, provisioned bool) *Result {
t.Helper()
t.Cleanup(func() {
fakeStore.provisionedData = nil
})
if provisioned {
fakeStore.provisionedData = &models.DashboardProvisioning{}
}
result := &Result{}
bus.AddHandler("test", func(cmd *models.DeleteDashboardCommand) error {
@@ -326,3 +331,32 @@ func setupDeleteHandlers(provisioned bool) *Result {
return result
}
type fakeDashboardStore struct {
dashboards.Store
validationError error
provisionedData *models.DashboardProvisioning
}
func (s *fakeDashboardStore) ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (
bool, error) {
return false, s.validationError
}
func (s *fakeDashboardStore) GetProvisionedDataByDashboardID(int64) (*models.DashboardProvisioning, error) {
return s.provisionedData, nil
}
func (s *fakeDashboardStore) SaveProvisionedDashboard(models.SaveDashboardCommand,
*models.DashboardProvisioning) (*models.Dashboard, error) {
return nil, nil
}
func (s *fakeDashboardStore) SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error) {
return cmd.GetDashboardModel(), nil
}
func (s *fakeDashboardStore) SaveAlerts(dashID int64, alerts []*models.Alert) error {
return nil
}
+29 -19
View File
@@ -2,28 +2,32 @@ package dashboards
import (
"errors"
"strings"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/dashboards"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/search"
)
// FolderService service for operating on folders
// FolderService is a service for operating on folders.
type FolderService interface {
GetFolders(limit int64) ([]*models.Folder, error)
GetFolderByID(id int64) (*models.Folder, error)
GetFolderByUID(uid string) (*models.Folder, error)
CreateFolder(cmd *models.CreateFolderCommand) error
CreateFolder(title, uid string) (*models.Folder, error)
UpdateFolder(uid string, cmd *models.UpdateFolderCommand) error
DeleteFolder(uid string) (*models.Folder, error)
MakeUserAdmin(orgID int64, userID, folderID int64, setViewAndEditPermissions bool) error
}
// NewFolderService factory for creating a new folder service
var NewFolderService = func(orgId int64, user *models.SignedInUser) FolderService {
// NewFolderService is a factory for creating a new folder service.
var NewFolderService = func(orgID int64, user *models.SignedInUser, store dashboards.Store) FolderService {
return &dashboardServiceImpl{
orgId: orgId,
user: user,
orgId: orgID,
user: user,
dashboardStore: store,
}
}
@@ -58,7 +62,6 @@ func (dr *dashboardServiceImpl) GetFolders(limit int64) ([]*models.Folder, error
func (dr *dashboardServiceImpl) GetFolderByID(id int64) (*models.Folder, error) {
query := models.GetDashboardQuery{OrgId: dr.orgId, Id: id}
dashFolder, err := getFolder(query)
if err != nil {
return nil, toFolderError(err)
}
@@ -93,8 +96,17 @@ func (dr *dashboardServiceImpl) GetFolderByUID(uid string) (*models.Folder, erro
return dashToFolder(dashFolder), nil
}
func (dr *dashboardServiceImpl) CreateFolder(cmd *models.CreateFolderCommand) error {
dashFolder := cmd.GetDashboardModel(dr.orgId, dr.user.UserId)
func (dr *dashboardServiceImpl) CreateFolder(title, uid string) (*models.Folder, error) {
dashFolder := models.NewDashboardFolder(title)
dashFolder.OrgId = dr.orgId
dashFolder.SetUid(strings.TrimSpace(uid))
userID := dr.user.UserId
if userID == 0 {
userID = -1
}
dashFolder.CreatedBy = userID
dashFolder.UpdatedBy = userID
dashFolder.UpdateSlug()
dto := &SaveDashboardDTO{
Dashboard: dashFolder,
@@ -104,23 +116,21 @@ func (dr *dashboardServiceImpl) CreateFolder(cmd *models.CreateFolderCommand) er
saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false, false)
if err != nil {
return toFolderError(err)
return nil, toFolderError(err)
}
err = bus.Dispatch(saveDashboardCmd)
dash, err := dr.dashboardStore.SaveDashboard(*saveDashboardCmd)
if err != nil {
return toFolderError(err)
return nil, toFolderError(err)
}
query := models.GetDashboardQuery{OrgId: dr.orgId, Id: saveDashboardCmd.Result.Id}
query := models.GetDashboardQuery{OrgId: dr.orgId, Id: dash.Id}
dashFolder, err = getFolder(query)
if err != nil {
return toFolderError(err)
return nil, toFolderError(err)
}
cmd.Result = dashToFolder(dashFolder)
return nil
return dashToFolder(dashFolder), nil
}
func (dr *dashboardServiceImpl) UpdateFolder(existingUid string, cmd *models.UpdateFolderCommand) error {
@@ -144,12 +154,12 @@ func (dr *dashboardServiceImpl) UpdateFolder(existingUid string, cmd *models.Upd
return toFolderError(err)
}
err = bus.Dispatch(saveDashboardCmd)
dash, err := dr.dashboardStore.SaveDashboard(*saveDashboardCmd)
if err != nil {
return toFolderError(err)
}
query = models.GetDashboardQuery{OrgId: dr.orgId, Id: saveDashboardCmd.Result.Id}
query = models.GetDashboardQuery{OrgId: dr.orgId, Id: dash.Id}
dashFolder, err = getFolder(query)
if err != nil {
return toFolderError(err)
+18 -29
View File
@@ -4,6 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/dashboards"
"github.com/grafana/grafana/pkg/models"
"github.com/stretchr/testify/assert"
@@ -15,8 +16,9 @@ import (
func TestFolderService(t *testing.T) {
Convey("Folder service tests", t, func() {
service := dashboardServiceImpl{
orgId: 1,
user: &models.SignedInUser{UserId: 1},
orgId: 1,
user: &models.SignedInUser{UserId: 1},
dashboardStore: &fakeDashboardStore{},
}
Convey("Given user has no permissions", func() {
@@ -28,32 +30,26 @@ func TestFolderService(t *testing.T) {
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
return models.ErrDashboardUpdateAccessDenied
origStore := service.dashboardStore
t.Cleanup(func() {
service.dashboardStore = origStore
})
service.dashboardStore = &fakeDashboardStore{
validationError: models.ErrDashboardUpdateAccessDenied,
}
Convey("When get folder by id should return access denied error", func() {
_, err := service.GetFolderByID(1)
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrFolderAccessDenied)
})
Convey("When get folder by uid should return access denied error", func() {
_, err := service.GetFolderByUID("uid")
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrFolderAccessDenied)
})
Convey("When creating folder should return access denied error", func() {
err := service.CreateFolder(&models.CreateFolderCommand{
Title: "Folder",
})
So(err, ShouldNotBeNil)
_, err := service.CreateFolder("Folder", "")
So(err, ShouldEqual, models.ErrFolderAccessDenied)
})
@@ -62,7 +58,6 @@ func TestFolderService(t *testing.T) {
Uid: "uid",
Title: "Folder",
})
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrFolderAccessDenied)
})
@@ -89,18 +84,14 @@ func TestFolderService(t *testing.T) {
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
origUpdateAlerting := UpdateAlerting
t.Cleanup(func() {
UpdateAlerting = origUpdateAlerting
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
UpdateAlerting = func(store dashboards.Store, orgID int64, dashboard *models.Dashboard,
user *models.SignedInUser) error {
return nil
})
bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error {
return nil
})
}
bus.AddHandler("test", func(cmd *models.SaveDashboardCommand) error {
cmd.Result = dash
@@ -120,9 +111,7 @@ func TestFolderService(t *testing.T) {
})
Convey("When creating folder should not return access denied error", func() {
err := service.CreateFolder(&models.CreateFolderCommand{
Title: "Folder",
})
_, err := service.CreateFolder("Folder", "")
So(err, ShouldBeNil)
So(provisioningValidated, ShouldBeFalse)
})