From cf86c696e03cc42bd247fe57c9f3df9c61407982 Mon Sep 17 00:00:00 2001 From: owensmallwood Date: Thu, 21 Jul 2022 13:56:20 -0600 Subject: [PATCH] Public Dashboards: Adds template variable validation for pubdash on the backend (#52566) Validates template variables for pubdash on the backend when saving a public dashboard --- pkg/services/publicdashboards/api/api.go | 1 + .../publicdashboards/database/database.go | 28 +++++--- .../database/database_test.go | 23 +++++++ .../publicdashboards/models/models.go | 4 ++ .../public_dashboard_service_mock.go | 23 +++++++ .../public_dashboard_store_mock.go | 69 ++++++++++++------- .../publicdashboards/publicdashboard.go | 2 + .../publicdashboards/service/service.go | 21 +++++- .../publicdashboards/service/service_test.go | 46 ++++++++++--- .../validation/validation_test.go | 60 ++++++++++++++++ .../publicdashboards/validation/validator.go | 27 ++++++++ 11 files changed, 258 insertions(+), 46 deletions(-) create mode 100644 pkg/services/publicdashboards/validation/validation_test.go create mode 100644 pkg/services/publicdashboards/validation/validator.go diff --git a/pkg/services/publicdashboards/api/api.go b/pkg/services/publicdashboards/api/api.go index 65d87fc6568..bec421898ea 100644 --- a/pkg/services/publicdashboards/api/api.go +++ b/pkg/services/publicdashboards/api/api.go @@ -126,6 +126,7 @@ func (api *Api) SavePublicDashboardConfig(c *models.ReqContext) response.Respons PublicDashboard: pubdash, } + // Save the public dashboard pubdash, err := api.PublicDashboardService.SavePublicDashboardConfig(c.Req.Context(), &dto) if err != nil { return handleDashboardErr(http.StatusInternalServerError, "Failed to save public dashboard configuration", err) diff --git a/pkg/services/publicdashboards/database/database.go b/pkg/services/publicdashboards/database/database.go index 9375b087a59..7c396e5cc07 100644 --- a/pkg/services/publicdashboards/database/database.go +++ b/pkg/services/publicdashboards/database/database.go @@ -34,6 +34,22 @@ func ProvideStore(sqlStore *sqlstore.SQLStore) *PublicDashboardStoreImpl { } } +func (d *PublicDashboardStoreImpl) GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) { + dashboard := &models.Dashboard{Uid: dashboardUid} + err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + has, err := sess.Get(dashboard) + if err != nil { + return err + } + if !has { + return ErrPublicDashboardNotFound + } + return nil + }) + + return dashboard, err +} + // Retrieves public dashboard configuration func (d *PublicDashboardStoreImpl) GetPublicDashboard(ctx context.Context, accessToken string) (*PublicDashboard, *models.Dashboard, error) { if accessToken == "" { @@ -58,17 +74,7 @@ func (d *PublicDashboardStoreImpl) GetPublicDashboard(ctx context.Context, acces } // find dashboard - dashRes := &models.Dashboard{OrgId: pdRes.OrgId, Uid: pdRes.DashboardUid} - err = d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { - has, err := sess.Get(dashRes) - if err != nil { - return err - } - if !has { - return ErrPublicDashboardNotFound - } - return nil - }) + dashRes, err := d.GetDashboard(ctx, pdRes.DashboardUid) if err != nil { return nil, nil, err diff --git a/pkg/services/publicdashboards/database/database_test.go b/pkg/services/publicdashboards/database/database_test.go index 801c2cb461f..49cc2f370df 100644 --- a/pkg/services/publicdashboards/database/database_test.go +++ b/pkg/services/publicdashboards/database/database_test.go @@ -24,6 +24,29 @@ var DefaultTimeSettings, _ = simplejson.NewJson([]byte(`{}`)) // Default time to pass in with seconds rounded var DefaultTime = time.Now().UTC().Round(time.Second) +func TestIntegrationGetDashboard(t *testing.T) { + var sqlStore *sqlstore.SQLStore + var dashboardStore *dashboardsDB.DashboardStore + var publicdashboardStore *PublicDashboardStoreImpl + var savedDashboard *models.Dashboard + + setup := func() { + sqlStore = sqlstore.InitTestDB(t) + dashboardStore = dashboardsDB.ProvideDashboardStore(sqlStore) + publicdashboardStore = ProvideStore(sqlStore) + savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) + } + + t.Run("GetDashboard can get original dashboard by uid", func(t *testing.T) { + setup() + + dashboard, err := publicdashboardStore.GetDashboard(context.Background(), savedDashboard.Uid) + + require.NoError(t, err) + require.Equal(t, savedDashboard.Uid, dashboard.Uid) + }) +} + // GetPublicDashboard func TestIntegrationGetPublicDashboard(t *testing.T) { var sqlStore *sqlstore.SQLStore diff --git a/pkg/services/publicdashboards/models/models.go b/pkg/services/publicdashboards/models/models.go index 31983d53088..c90502fe827 100644 --- a/pkg/services/publicdashboards/models/models.go +++ b/pkg/services/publicdashboards/models/models.go @@ -45,6 +45,10 @@ var ( Reason: "No Uid for public dashboard specified", StatusCode: 400, } + ErrPublicDashboardHasTemplateVariables = PublicDashboardErr{ + Reason: "Public dashboard has template variables", + StatusCode: 422, + } ) type PublicDashboard struct { diff --git a/pkg/services/publicdashboards/public_dashboard_service_mock.go b/pkg/services/publicdashboards/public_dashboard_service_mock.go index f071f3df777..d5ee71d3ab4 100644 --- a/pkg/services/publicdashboards/public_dashboard_service_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_service_mock.go @@ -64,6 +64,29 @@ func (_m *FakePublicDashboardService) BuildPublicDashboardMetricRequest(ctx cont return r0, r1 } +// GetDashboard provides a mock function with given fields: ctx, dashboardUid +func (_m *FakePublicDashboardService) GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) { + ret := _m.Called(ctx, dashboardUid) + + var r0 *models.Dashboard + if rf, ok := ret.Get(0).(func(context.Context, string) *models.Dashboard); ok { + r0 = rf(ctx, dashboardUid) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Dashboard) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, dashboardUid) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetPublicDashboard provides a mock function with given fields: ctx, accessToken func (_m *FakePublicDashboardService) GetPublicDashboard(ctx context.Context, accessToken string) (*models.Dashboard, error) { ret := _m.Called(ctx, accessToken) diff --git a/pkg/services/publicdashboards/public_dashboard_store_mock.go b/pkg/services/publicdashboards/public_dashboard_store_mock.go index 9ceaf435371..e36231051ef 100644 --- a/pkg/services/publicdashboards/public_dashboard_store_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_store_mock.go @@ -5,10 +5,10 @@ package publicdashboards import ( context "context" - models "github.com/grafana/grafana/pkg/services/publicdashboards/models" + models "github.com/grafana/grafana/pkg/models" mock "github.com/stretchr/testify/mock" - pkgmodels "github.com/grafana/grafana/pkg/models" + publicdashboardsmodels "github.com/grafana/grafana/pkg/services/publicdashboards/models" testing "testing" ) @@ -39,25 +39,48 @@ func (_m *FakePublicDashboardStore) GenerateNewPublicDashboardUid(ctx context.Co return r0, r1 } -// GetPublicDashboard provides a mock function with given fields: ctx, accessToken -func (_m *FakePublicDashboardStore) GetPublicDashboard(ctx context.Context, accessToken string) (*models.PublicDashboard, *pkgmodels.Dashboard, error) { - ret := _m.Called(ctx, accessToken) +// GetDashboard provides a mock function with given fields: ctx, dashboardUid +func (_m *FakePublicDashboardStore) GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) { + ret := _m.Called(ctx, dashboardUid) - var r0 *models.PublicDashboard - if rf, ok := ret.Get(0).(func(context.Context, string) *models.PublicDashboard); ok { - r0 = rf(ctx, accessToken) + var r0 *models.Dashboard + if rf, ok := ret.Get(0).(func(context.Context, string) *models.Dashboard); ok { + r0 = rf(ctx, dashboardUid) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.PublicDashboard) + r0 = ret.Get(0).(*models.Dashboard) } } - var r1 *pkgmodels.Dashboard - if rf, ok := ret.Get(1).(func(context.Context, string) *pkgmodels.Dashboard); ok { + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, dashboardUid) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetPublicDashboard provides a mock function with given fields: ctx, accessToken +func (_m *FakePublicDashboardStore) GetPublicDashboard(ctx context.Context, accessToken string) (*publicdashboardsmodels.PublicDashboard, *models.Dashboard, error) { + ret := _m.Called(ctx, accessToken) + + var r0 *publicdashboardsmodels.PublicDashboard + if rf, ok := ret.Get(0).(func(context.Context, string) *publicdashboardsmodels.PublicDashboard); ok { + r0 = rf(ctx, accessToken) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*publicdashboardsmodels.PublicDashboard) + } + } + + var r1 *models.Dashboard + if rf, ok := ret.Get(1).(func(context.Context, string) *models.Dashboard); ok { r1 = rf(ctx, accessToken) } else { if ret.Get(1) != nil { - r1 = ret.Get(1).(*pkgmodels.Dashboard) + r1 = ret.Get(1).(*models.Dashboard) } } @@ -72,15 +95,15 @@ func (_m *FakePublicDashboardStore) GetPublicDashboard(ctx context.Context, acce } // GetPublicDashboardConfig provides a mock function with given fields: ctx, orgId, dashboardUid -func (_m *FakePublicDashboardStore) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) { +func (_m *FakePublicDashboardStore) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*publicdashboardsmodels.PublicDashboard, error) { ret := _m.Called(ctx, orgId, dashboardUid) - var r0 *models.PublicDashboard - if rf, ok := ret.Get(0).(func(context.Context, int64, string) *models.PublicDashboard); ok { + var r0 *publicdashboardsmodels.PublicDashboard + if rf, ok := ret.Get(0).(func(context.Context, int64, string) *publicdashboardsmodels.PublicDashboard); ok { r0 = rf(ctx, orgId, dashboardUid) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.PublicDashboard) + r0 = ret.Get(0).(*publicdashboardsmodels.PublicDashboard) } } @@ -116,20 +139,20 @@ func (_m *FakePublicDashboardStore) PublicDashboardEnabled(ctx context.Context, } // SavePublicDashboardConfig provides a mock function with given fields: ctx, cmd -func (_m *FakePublicDashboardStore) SavePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboard, error) { +func (_m *FakePublicDashboardStore) SavePublicDashboardConfig(ctx context.Context, cmd publicdashboardsmodels.SavePublicDashboardConfigCommand) (*publicdashboardsmodels.PublicDashboard, error) { ret := _m.Called(ctx, cmd) - var r0 *models.PublicDashboard - if rf, ok := ret.Get(0).(func(context.Context, models.SavePublicDashboardConfigCommand) *models.PublicDashboard); ok { + var r0 *publicdashboardsmodels.PublicDashboard + if rf, ok := ret.Get(0).(func(context.Context, publicdashboardsmodels.SavePublicDashboardConfigCommand) *publicdashboardsmodels.PublicDashboard); ok { r0 = rf(ctx, cmd) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.PublicDashboard) + r0 = ret.Get(0).(*publicdashboardsmodels.PublicDashboard) } } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, models.SavePublicDashboardConfigCommand) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, publicdashboardsmodels.SavePublicDashboardConfigCommand) error); ok { r1 = rf(ctx, cmd) } else { r1 = ret.Error(1) @@ -139,11 +162,11 @@ func (_m *FakePublicDashboardStore) SavePublicDashboardConfig(ctx context.Contex } // UpdatePublicDashboardConfig provides a mock function with given fields: ctx, cmd -func (_m *FakePublicDashboardStore) UpdatePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) error { +func (_m *FakePublicDashboardStore) UpdatePublicDashboardConfig(ctx context.Context, cmd publicdashboardsmodels.SavePublicDashboardConfigCommand) error { ret := _m.Called(ctx, cmd) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, models.SavePublicDashboardConfigCommand) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, publicdashboardsmodels.SavePublicDashboardConfigCommand) error); ok { r0 = rf(ctx, cmd) } else { r0 = ret.Error(0) diff --git a/pkg/services/publicdashboards/publicdashboard.go b/pkg/services/publicdashboards/publicdashboard.go index 33fdffd6ec6..c1c6e9feecd 100644 --- a/pkg/services/publicdashboards/publicdashboard.go +++ b/pkg/services/publicdashboards/publicdashboard.go @@ -14,6 +14,7 @@ import ( type Service interface { BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) (*models.SignedInUser, error) GetPublicDashboard(ctx context.Context, accessToken string) (*models.Dashboard, error) + GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error) SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*PublicDashboard, error) BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64) (dtos.MetricRequest, error) @@ -23,6 +24,7 @@ type Service interface { //go:generate mockery --name Store --structname FakePublicDashboardStore --inpackage --filename public_dashboard_store_mock.go type Store interface { GetPublicDashboard(ctx context.Context, accessToken string) (*PublicDashboard, *models.Dashboard, error) + GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error) GenerateNewPublicDashboardUid(ctx context.Context) (string, error) SavePublicDashboardConfig(ctx context.Context, cmd SavePublicDashboardConfigCommand) (*PublicDashboard, error) diff --git a/pkg/services/publicdashboards/service/service.go b/pkg/services/publicdashboards/service/service.go index caa7149858e..2b3a2168390 100644 --- a/pkg/services/publicdashboards/service/service.go +++ b/pkg/services/publicdashboards/service/service.go @@ -10,10 +10,10 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/publicdashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" + "github.com/grafana/grafana/pkg/services/publicdashboards/validation" "github.com/grafana/grafana/pkg/setting" ) @@ -42,6 +42,16 @@ func ProvideService( } } +func (pd *PublicDashboardServiceImpl) GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) { + dashboard, err := pd.store.GetDashboard(ctx, dashboardUid) + + if err != nil { + return nil, err + } + + return dashboard, err +} + // Gets public dashboard via access token func (pd *PublicDashboardServiceImpl) GetPublicDashboard(ctx context.Context, accessToken string) (*models.Dashboard, error) { pubdash, d, err := pd.store.GetPublicDashboard(ctx, accessToken) @@ -78,8 +88,13 @@ func (pd *PublicDashboardServiceImpl) GetPublicDashboardConfig(ctx context.Conte // SavePublicDashboardConfig is a helper method to persist the sharing config // to the database. It handles validations for sharing config and persistence func (pd *PublicDashboardServiceImpl) SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*PublicDashboard, error) { - if len(dto.DashboardUid) == 0 { - return nil, dashboards.ErrDashboardIdentifierNotSet + dashboard, err := pd.GetDashboard(ctx, dto.DashboardUid) + if err != nil { + return nil, err + } + err = validation.ValidateSavePublicDashboard(dto, dashboard) + if err != nil { + return nil, err } // set default value for time settings diff --git a/pkg/services/publicdashboards/service/service_test.go b/pkg/services/publicdashboards/service/service_test.go index 8198d41f4fa..519c5cdce84 100644 --- a/pkg/services/publicdashboards/service/service_test.go +++ b/pkg/services/publicdashboards/service/service_test.go @@ -120,7 +120,7 @@ func TestSavePublicDashboard(t *testing.T) { sqlStore := sqlstore.InitTestDB(t) dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore) publicdashboardStore := database.ProvideStore(sqlStore) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}) service := &PublicDashboardServiceImpl{ log: log.New("test.logger"), @@ -164,7 +164,7 @@ func TestSavePublicDashboard(t *testing.T) { sqlStore := sqlstore.InitTestDB(t) dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore) publicdashboardStore := database.ProvideStore(sqlStore) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}) service := &PublicDashboardServiceImpl{ log: log.New("test.logger"), @@ -190,7 +190,32 @@ func TestSavePublicDashboard(t *testing.T) { assert.Equal(t, defaultPubdashTimeSettings, pubdash.TimeSettings) }) - t.Run("PLACEHOLDER - dashboard with template variables cannot be saved", func(t *testing.T) {}) + t.Run("Validate pubdash whose dashboard has template variables returns error", func(t *testing.T) { + sqlStore := sqlstore.InitTestDB(t) + dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore) + publicdashboardStore := database.ProvideStore(sqlStore) + templateVars := make([]map[string]interface{}, 1) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, templateVars) + + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: publicdashboardStore, + } + + dto := &SavePublicDashboardConfigDTO{ + DashboardUid: dashboard.Uid, + OrgId: dashboard.OrgId, + UserId: 7, + PublicDashboard: &PublicDashboard{ + IsEnabled: true, + DashboardUid: "NOTTHESAME", + OrgId: 9999999, + }, + } + + _, err := service.SavePublicDashboardConfig(context.Background(), dto) + require.Error(t, err) + }) } func TestUpdatePublicDashboard(t *testing.T) { @@ -198,7 +223,7 @@ func TestUpdatePublicDashboard(t *testing.T) { sqlStore := sqlstore.InitTestDB(t) dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore) publicdashboardStore := database.ProvideStore(sqlStore) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}) service := &PublicDashboardServiceImpl{ log: log.New("test.logger"), @@ -265,7 +290,7 @@ func TestUpdatePublicDashboard(t *testing.T) { sqlStore := sqlstore.InitTestDB(t) dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore) publicdashboardStore := database.ProvideStore(sqlStore) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}) service := &PublicDashboardServiceImpl{ log: log.New("test.logger"), @@ -323,7 +348,7 @@ func TestUpdatePublicDashboard(t *testing.T) { func TestBuildAnonymousUser(t *testing.T) { sqlStore := sqlstore.InitTestDB(t) dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}) publicdashboardStore := database.ProvideStore(sqlStore) service := &PublicDashboardServiceImpl{ log: log.New("test.logger"), @@ -346,8 +371,8 @@ func TestBuildPublicDashboardMetricRequest(t *testing.T) { dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore) publicdashboardStore := database.ProvideStore(sqlStore) - publicDashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) - nonPublicDashboard := insertTestDashboard(t, dashboardStore, "testNonPublicDashie", 1, 0, true) + publicDashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}) + nonPublicDashboard := insertTestDashboard(t, dashboardStore, "testNonPublicDashie", 1, 0, true, []map[string]interface{}{}) service := &PublicDashboardServiceImpl{ log: log.New("test.logger"), @@ -441,7 +466,7 @@ func TestBuildPublicDashboardMetricRequest(t *testing.T) { } func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardStore, title string, orgId int64, - folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard { + folderId int64, isFolder bool, templateVars []map[string]interface{}, tags ...interface{}) *models.Dashboard { t.Helper() cmd := models.SaveDashboardCommand{ OrgId: orgId, @@ -490,6 +515,9 @@ func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardSto }, }, }, + "templating": map[string]interface{}{ + "list": templateVars, + }, }), } dash, err := dashboardStore.SaveDashboard(cmd) diff --git a/pkg/services/publicdashboards/validation/validation_test.go b/pkg/services/publicdashboards/validation/validation_test.go new file mode 100644 index 00000000000..668e844c0af --- /dev/null +++ b/pkg/services/publicdashboards/validation/validation_test.go @@ -0,0 +1,60 @@ +package validation + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/models" + publicdashboardModels "github.com/grafana/grafana/pkg/services/publicdashboards/models" + "github.com/stretchr/testify/require" +) + +func TestValidateSavePublicDashboard(t *testing.T) { + t.Run("Returns validation error when dto has no dashboard uid", func(t *testing.T) { + dashboard := models.NewDashboard("dashboardTitle") + dto := &publicdashboardModels.SavePublicDashboardConfigDTO{DashboardUid: "", OrgId: 1, UserId: 1, PublicDashboard: nil} + + err := ValidateSavePublicDashboard(dto, dashboard) + require.ErrorContains(t, err, "Unique identifier needed to be able to get a dashboard") + }) + + t.Run("Returns no validation error when dto has dashboard uid", func(t *testing.T) { + dashboard := models.NewDashboard("dashboardTitle") + dto := &publicdashboardModels.SavePublicDashboardConfigDTO{DashboardUid: "abc123", OrgId: 1, UserId: 1, PublicDashboard: nil} + + err := ValidateSavePublicDashboard(dto, dashboard) + require.NoError(t, err) + }) + + t.Run("Returns validation error when dashboard has template variables", func(t *testing.T) { + templateVars := []byte(`{ + "templating": { + "list": [ + { + "name": "templateVariableName" + } + ] + } + }`) + dashboardData, _ := simplejson.NewJson(templateVars) + dashboard := models.NewDashboardFromJson(dashboardData) + dto := &publicdashboardModels.SavePublicDashboardConfigDTO{DashboardUid: "abc123", OrgId: 1, UserId: 1, PublicDashboard: nil} + + err := ValidateSavePublicDashboard(dto, dashboard) + require.ErrorContains(t, err, "Public dashboard has template variables") + }) + + t.Run("Returns no validation error when dashboard has no template variables", func(t *testing.T) { + templateVars := []byte(`{ + "templating": { + "list": [] + } + }`) + dashboardData, _ := simplejson.NewJson(templateVars) + dashboard := models.NewDashboardFromJson(dashboardData) + dto := &publicdashboardModels.SavePublicDashboardConfigDTO{DashboardUid: "abc123", OrgId: 1, UserId: 1, PublicDashboard: nil} + + err := ValidateSavePublicDashboard(dto, dashboard) + require.NoError(t, err) + }) +} diff --git a/pkg/services/publicdashboards/validation/validator.go b/pkg/services/publicdashboards/validation/validator.go new file mode 100644 index 00000000000..63d84d93569 --- /dev/null +++ b/pkg/services/publicdashboards/validation/validator.go @@ -0,0 +1,27 @@ +package validation + +import ( + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" + publicDashboardModels "github.com/grafana/grafana/pkg/services/publicdashboards/models" +) + +func ValidateSavePublicDashboard(dto *publicDashboardModels.SavePublicDashboardConfigDTO, dashboard *models.Dashboard) error { + var err error + + if len(dto.DashboardUid) == 0 { + return dashboards.ErrDashboardIdentifierNotSet + } + + if hasTemplateVariables(dashboard) { + return publicDashboardModels.ErrPublicDashboardHasTemplateVariables + } + + return err +} + +func hasTemplateVariables(dashboard *models.Dashboard) bool { + templateVariables := dashboard.Data.Get("templating").Get("list").MustArray() + + return len(templateVariables) > 0 +}