API: prevent provisioned dashboard from being updated (#41894)

pull/43151/head^2
Sofia Papagiannaki 3 years ago committed by GitHub
parent db18acff15
commit c4aaf5f9d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      pkg/api/dashboard.go
  2. 1
      pkg/dashboards/ifaces.go
  3. 5
      pkg/models/dashboards.go
  4. 5
      pkg/services/dashboards/dashboard_service.go
  5. 24
      pkg/services/sqlstore/dashboard_provisioning.go
  6. 134
      pkg/tests/api/dashboards/api_dashboards_test.go
  7. 231
      pkg/tests/api/dashboards/home.json

@ -319,9 +319,19 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa
}
svc := dashboards.NewProvisioningService(hs.SQLStore)
provisioningData, err := svc.GetProvisionedDashboardDataByDashboardID(dash.Id)
if err != nil {
return response.Error(500, "Error while checking if dashboard is provisioned", err)
var provisioningData *models.DashboardProvisioning
if dash.Id != 0 {
data, err := svc.GetProvisionedDashboardDataByDashboardID(dash.Id)
if err != nil {
return response.Error(500, "Error while checking if dashboard is provisioned", err)
}
provisioningData = data
} else if dash.Uid != "" {
data, err := svc.GetProvisionedDashboardDataByDashboardUID(dash.OrgId, dash.Uid)
if err != nil && !errors.Is(err, models.ErrProvisionedDashboardNotFound) {
return response.Error(500, "Error while checking if dashboard is provisioned", err)
}
provisioningData = data
}
allowUiUpdate := true

@ -13,6 +13,7 @@ type Store interface {
// GetFolderByTitle retrieves a dashboard by its title and is used by unified alerting
GetFolderByTitle(orgID int64, title string) (*models.Dashboard, error)
GetProvisionedDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error)
GetProvisionedDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error)
GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
SaveProvisionedDashboard(cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error)

@ -103,6 +103,11 @@ var (
Reason: "Unique identifier needed to be able to get a dashboard",
StatusCode: 400,
}
ErrProvisionedDashboardNotFound = DashboardErr{
Reason: "Dashboard is not provisioned",
StatusCode: 404,
Status: "not-found",
}
)
// DashboardErr represents a dashboard error.

@ -32,6 +32,7 @@ type DashboardProvisioningService interface {
SaveProvisionedDashboard(ctx context.Context, dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
SaveFolderForProvisionedDashboards(context.Context, *SaveDashboardDTO) (*models.Dashboard, error)
GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
GetProvisionedDashboardDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error)
GetProvisionedDashboardDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error)
UnprovisionDashboard(ctx context.Context, dashboardID int64) error
DeleteProvisionedDashboard(ctx context.Context, dashboardID int64, orgID int64) error
@ -81,6 +82,10 @@ func (dr *dashboardServiceImpl) GetProvisionedDashboardDataByDashboardID(dashboa
return GetProvisionedData(dr.dashboardStore, dashboardID)
}
func (dr *dashboardServiceImpl) GetProvisionedDashboardDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error) {
return dr.dashboardStore.GetProvisionedDataByDashboardUID(orgID, dashboardUID)
}
func (dr *dashboardServiceImpl) buildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, shouldValidateAlerts bool,
validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) {
dash := dto.Dashboard

@ -32,6 +32,30 @@ func (ss *SQLStore) GetProvisionedDataByDashboardID(dashboardID int64) (*models.
return nil, nil
}
func (ss *SQLStore) GetProvisionedDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error) {
var provisionedDashboard models.DashboardProvisioning
err := ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error {
var dashboard models.Dashboard
exists, err := sess.Where("org_id = ? AND uid = ?", orgID, dashboardUID).Get(&dashboard)
if err != nil {
return err
}
if !exists {
return models.
ErrDashboardNotFound
}
exists, err = sess.Where("dashboard_id = ?", dashboard.Id).Get(&provisionedDashboard)
if err != nil {
return err
}
if !exists {
return models.ErrProvisionedDashboardNotFound
}
return nil
})
return &provisionedDashboard, err
}
func (ss *SQLStore) SaveProvisionedDashboard(cmd models.SaveDashboardCommand,
provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
err := ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error {

@ -7,12 +7,16 @@ import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"testing"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/stretchr/testify/assert"
@ -92,3 +96,133 @@ func createUser(t *testing.T, store *sqlstore.SQLStore, cmd models.CreateUserCom
require.NoError(t, err)
return u.Id
}
func TestProvisionioningDashboards(t *testing.T) {
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableAnonymous: true,
})
provDashboardsDir := filepath.Join(dir, "conf", "provisioning", "dashboards")
provDashboardsCfg := filepath.Join(provDashboardsDir, "dev.yaml")
blob := []byte(fmt.Sprintf(`
apiVersion: 1
providers:
- name: 'provisioned dashboards'
type: file
allowUiUpdates: false
options:
path: %s`, provDashboardsDir))
err := os.WriteFile(provDashboardsCfg, blob, 0644)
require.NoError(t, err)
input, err := ioutil.ReadFile(filepath.Join("./home.json"))
require.NoError(t, err)
provDashboardFile := filepath.Join(provDashboardsDir, "home.json")
err = ioutil.WriteFile(provDashboardFile, input, 0644)
require.NoError(t, err)
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path)
// Create user
createUser(t, store, models.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_ADMIN),
Password: "admin",
Login: "admin",
})
type errorResponseBody struct {
Message string `json:"message"`
}
t.Run("when provisioned directory is not empty, dashboard should be created", func(t *testing.T) {
title := "Grafana Dev Overview & Home"
u := fmt.Sprintf("http://admin:admin@%s/api/search?query=%s", grafanaListedAddr, url.QueryEscape(title))
// nolint:gosec
resp, err := http.Get(u)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
dashboardList := &search.HitList{}
err = json.Unmarshal(b, dashboardList)
require.NoError(t, err)
assert.Equal(t, 1, dashboardList.Len())
var dashboardUID string
var dashboardID int64
for _, d := range *dashboardList {
dashboardUID = d.UID
dashboardID = d.ID
}
assert.Equal(t, int64(1), dashboardID)
testCases := []struct {
desc string
dashboardData string
}{
{
desc: "when updating provisioned dashboard using ID it should fail",
dashboardData: fmt.Sprintf(`{"title":"just testing", "id": %d, "version": 1}`, dashboardID),
},
{
desc: "when updating provisioned dashboard using UID is should fail",
dashboardData: fmt.Sprintf(`{"title":"just testing", "uid": %q, "version": 1}`, dashboardUID),
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
u := fmt.Sprintf("http://admin:admin@%s/api/dashboards/db", grafanaListedAddr)
// nolint:gosec
dashboardData, err := simplejson.NewJson([]byte(tc.dashboardData))
require.NoError(t, err)
buf := &bytes.Buffer{}
err = json.NewEncoder(buf).Encode(models.SaveDashboardCommand{
Dashboard: dashboardData,
})
require.NoError(t, err)
// nolint:gosec
resp, err := http.Post(u, "application/json", buf)
require.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
dashboardErr := &errorResponseBody{}
err = json.Unmarshal(b, dashboardErr)
require.NoError(t, err)
assert.Equal(t, models.ErrDashboardCannotSaveProvisionedDashboard.Reason, dashboardErr.Message)
})
}
t.Run("deleting provisioned dashboard should fail", func(t *testing.T) {
u := fmt.Sprintf("http://admin:admin@%s/api/dashboards/uid/%s", grafanaListedAddr, dashboardUID)
req, err := http.NewRequest("DELETE", u, nil)
if err != nil {
fmt.Println(err)
return
}
client := &http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
b, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
dashboardErr := &errorResponseBody{}
err = json.Unmarshal(b, dashboardErr)
require.NoError(t, err)
assert.Equal(t, models.ErrDashboardCannotDeleteProvisionedDashboard.Reason, dashboardErr.Message)
})
})
}

@ -0,0 +1,231 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"graphTooltip": 0,
"links": [],
"panels": [
{
"gridPos": {
"h": 26,
"w": 6,
"x": 0,
"y": 0
},
"id": 7,
"links": [],
"options": {
"maxItems": 100,
"query": "",
"showHeadings": true,
"showRecentlyViewed": true,
"showSearch": false,
"showStarred": true,
"tags": []
},
"pluginVersion": "8.1.0-pre",
"tags": [],
"title": "Starred",
"type": "dashlist"
},
{
"gridPos": {
"h": 13,
"w": 6,
"x": 6,
"y": 0
},
"id": 2,
"links": [],
"options": {
"maxItems": 1000,
"query": "",
"showHeadings": false,
"showRecentlyViewed": false,
"showSearch": true,
"showStarred": false,
"tags": [
"panel-tests"
]
},
"pluginVersion": "8.1.0-pre",
"tags": [
"panel-tests"
],
"title": "tag: panel-tests",
"type": "dashlist"
},
{
"gridPos": {
"h": 13,
"w": 6,
"x": 12,
"y": 0
},
"id": 3,
"links": [],
"options": {
"maxItems": 1000,
"query": "",
"showHeadings": false,
"showRecentlyViewed": false,
"showSearch": true,
"showStarred": false,
"tags": [
"gdev",
"demo"
]
},
"pluginVersion": "8.1.0-pre",
"tags": [
"gdev",
"demo"
],
"title": "tag: dashboard-demo",
"type": "dashlist"
},
{
"gridPos": {
"h": 26,
"w": 6,
"x": 18,
"y": 0
},
"id": 5,
"links": [],
"options": {
"maxItems": 1000,
"query": "",
"showHeadings": false,
"showRecentlyViewed": false,
"showSearch": true,
"showStarred": false,
"tags": [
"gdev",
"datasource-test"
]
},
"pluginVersion": "8.1.0-pre",
"tags": [
"gdev",
"datasource-test"
],
"title": "Data source tests",
"type": "dashlist"
},
{
"gridPos": {
"h": 13,
"w": 6,
"x": 6,
"y": 13
},
"id": 4,
"links": [],
"options": {
"maxItems": 1000,
"query": "",
"showHeadings": false,
"showRecentlyViewed": false,
"showSearch": true,
"showStarred": false,
"tags": [
"templating",
"gdev"
]
},
"pluginVersion": "8.1.0-pre",
"tags": [
"templating",
"gdev"
],
"title": "tag: templating ",
"type": "dashlist"
},
{
"gridPos": {
"h": 13,
"w": 6,
"x": 12,
"y": 13
},
"id": 8,
"links": [],
"options": {
"maxItems": 1000,
"query": "",
"showHeadings": false,
"showRecentlyViewed": false,
"showSearch": true,
"showStarred": false,
"tags": [
"gdev",
"transform"
]
},
"pluginVersion": "8.1.0-pre",
"tags": [
"gdev",
"demo"
],
"title": "tag: transforms",
"type": "dashlist"
}
],
"schemaVersion": 30,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"timezone": "",
"title": "Grafana Dev Overview & Home",
"uid": "j6T00KRZz",
"version": 2
}
Loading…
Cancel
Save