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
This commit is contained in:
owensmallwood 2022-07-21 13:56:20 -06:00 committed by GitHub
parent 9ef29bb5c3
commit cf86c696e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 258 additions and 46 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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