Shouldn't be able to overwrite a dashboard if you don't have permissions (#10900)

* dashboards: new command for validating dashboard before update

Removes validation logic from saveDashboard and later on use the new command for validating
dashboard before saving a dashboard. This due to the fact that we need to validate permissions
for overwriting other dashboards by uid and title.

* dashboards: use the new command for validating dashboard before saving

Had to refactor dashboard provisioning a bit to be able to sidetrack the permission validation
in a somewhat reasonable way.
Adds some initial tests of the dashboard repository, but needs to be extended later. At least
now you can mock the dashboard guardian

* dashboards: removes validation logic in the save dashboard api layer

Use the dashboard repository solely for create/update dashboards and let it do all
the validation. One exception regarding quota validation which still is in api layer
since that logic is in a macaron middleware.
Need to move out-commented api tests later.

* dashboards: fix database tests for validate and saving dashboards

* dashboards: rename dashboard repository to dashboard service

Split the old dashboard repository interface in two new interfaces, IDashboardService and
IDashboardProvisioningService. Makes it more explicit when using it from the provisioning package
and there's no possibility of calling an incorrect method for saving a dashboard.

* database: make the InitTestDB function available to use from other packages

* dashboards: rename ValidateDashboardForUpdateCommand and some refactoring

* dashboards: integration tests of dashboard service

* dashboard: fix sqlstore test due to folder exist validation

* dashboards: move dashboard service integration tests to sqlstore package

Had to move it to the sqlstore package due to concurrency problems when running
against mysql and postgres. Using InitTestDB from two packages added conflicts
when clearing and running migrations on the test database

* dashboards: refactor how to find id to be used for save permission check

* dashboards: remove duplicated dashboard tests

* dashboards: cleanup dashboard service integration tests

* dashboards: handle save dashboard errors and return correct http status

* fix: remove log statement

* dashboards: import dashboard should use dashboard service

Had to move alerting commands to models package due to problems with import cycles of packages.

* dashboards: cleanup dashboard api tests and add some tests for post dashboard

* dashboards: rename dashboard service interfaces

* dashboards: rename dashboard guardian interface
This commit is contained in:
Marcus Efraimsson
2018-02-19 11:12:56 +01:00
committed by Torkel Ödegaard
parent 63f8854a48
commit 53cd39fde5
27 changed files with 1896 additions and 937 deletions

View File

@@ -226,7 +226,7 @@ func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response {
return ApiError(500, "Get Alert failed", err)
}
guardian := guardian.NewDashboardGuardian(query.Result.DashboardId, c.OrgId, c.SignedInUser)
guardian := guardian.New(query.Result.DashboardId, c.OrgId, c.SignedInUser)
if canEdit, err := guardian.CanEdit(); err != nil || !canEdit {
if err != nil {
return ApiError(500, "Error while checking permissions for Alert", err)

View File

@@ -278,7 +278,7 @@ func canSaveByDashboardId(c *middleware.Context, dashboardId int64) (bool, error
}
if dashboardId > 0 {
guardian := guardian.NewDashboardGuardian(dashboardId, c.OrgId, c.SignedInUser)
guardian := guardian.New(dashboardId, c.OrgId, c.SignedInUser)
if canEdit, err := guardian.CanEdit(); err != nil || !canEdit {
return false, err
}

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"os"
"path"
"strings"
"github.com/grafana/grafana/pkg/services/dashboards"
@@ -50,7 +49,7 @@ func GetDashboard(c *middleware.Context) Response {
return rsp
}
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
guardian := guardian.New(dash.Id, c.OrgId, c.SignedInUser)
if canView, err := guardian.CanView(); err != nil || !canView {
return dashboardGuardianResponse(err)
}
@@ -157,7 +156,7 @@ func DeleteDashboard(c *middleware.Context) Response {
return rsp
}
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
guardian := guardian.New(dash.Id, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
@@ -177,7 +176,7 @@ func DeleteDashboardByUid(c *middleware.Context) Response {
return rsp
}
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
guardian := guardian.New(dash.Id, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
@@ -197,32 +196,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
dash := cmd.GetDashboardModel()
dashId := dash.Id
// if new dashboard, use parent folder permissions instead
if dashId == 0 {
dashId = cmd.FolderId
}
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
if dash.IsFolder && dash.FolderId > 0 {
return ApiError(400, m.ErrDashboardFolderCannotHaveParent.Error(), nil)
}
// Check if Title is empty
if dash.Title == "" {
return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil)
}
if dash.IsFolder && strings.ToLower(dash.Title) == strings.ToLower(m.RootFolderName) {
return ApiError(400, "A folder already exists with that name", nil)
}
if dash.Id == 0 {
if dash.Id == 0 && dash.Uid == "" {
limitReached, err := middleware.QuotaReached(c, "dashboard")
if err != nil {
return ApiError(500, "failed to get quota", err)
@@ -236,27 +210,34 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
Dashboard: dash,
Message: cmd.Message,
OrgId: c.OrgId,
UserId: c.UserId,
User: c.SignedInUser,
Overwrite: cmd.Overwrite,
}
dashboard, err := dashboards.GetRepository().SaveDashboard(dashItem)
dashboard, err := dashboards.NewService().SaveDashboard(dashItem)
if err == m.ErrDashboardTitleEmpty ||
err == m.ErrDashboardWithSameNameAsFolder ||
err == m.ErrDashboardFolderWithSameNameAsDashboard ||
err == m.ErrDashboardTypeMismatch {
err == m.ErrDashboardTypeMismatch ||
err == m.ErrDashboardInvalidUid ||
err == m.ErrDashboardUidToLong ||
err == m.ErrDashboardWithSameUIDExists ||
err == m.ErrFolderNotFound ||
err == m.ErrDashboardFolderCannotHaveParent ||
err == m.ErrDashboardFolderNameExists {
return ApiError(400, err.Error(), nil)
}
if err == m.ErrDashboardUpdateAccessDenied {
return ApiError(403, err.Error(), err)
}
if err == m.ErrDashboardContainsInvalidAlertData {
return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
}
if err != nil {
if err == m.ErrDashboardWithSameUIDExists {
return Json(412, util.DynMap{"status": "name-exists", "message": err.Error()})
}
if err == m.ErrDashboardWithSameNameInFolderExists {
return Json(412, util.DynMap{"status": "name-exists", "message": err.Error()})
}
@@ -281,8 +262,6 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
}
dashboard.IsFolder = dash.IsFolder
c.TimeRequest(metrics.M_Api_Dashboard_Save)
return Json(200, util.DynMap{
"status": "success",
@@ -357,7 +336,7 @@ func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
func GetDashboardVersions(c *middleware.Context) Response {
dashId := c.ParamsInt64(":dashboardId")
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
guardian := guardian.New(dashId, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
@@ -396,7 +375,7 @@ func GetDashboardVersions(c *middleware.Context) Response {
func GetDashboardVersion(c *middleware.Context) Response {
dashId := c.ParamsInt64(":dashboardId")
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
guardian := guardian.New(dashId, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
@@ -464,7 +443,7 @@ func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboard
return rsp
}
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
guardian := guardian.New(dash.Id, c.OrgId, c.SignedInUser)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}

View File

@@ -18,7 +18,7 @@ func GetDashboardAclList(c *middleware.Context) Response {
return rsp
}
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
guardian := guardian.New(dashId, c.OrgId, c.SignedInUser)
if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
return dashboardGuardianResponse(err)
@@ -46,7 +46,7 @@ func UpdateDashboardAcl(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCom
return rsp
}
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
guardian := guardian.New(dashId, c.OrgId, c.SignedInUser)
if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
return dashboardGuardianResponse(err)
}

View File

@@ -2,6 +2,7 @@ package api
import (
"encoding/json"
"fmt"
"testing"
"github.com/grafana/grafana/pkg/api/dtos"
@@ -9,38 +10,17 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
)
type fakeDashboardRepo struct {
inserted []*dashboards.SaveDashboardDTO
provisioned []*m.DashboardProvisioning
getDashboard []*m.Dashboard
}
func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardDTO) (*m.Dashboard, error) {
repo.inserted = append(repo.inserted, json)
return json.Dashboard, nil
}
func (repo *fakeDashboardRepo) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *m.DashboardProvisioning) (*m.Dashboard, error) {
repo.inserted = append(repo.inserted, dto)
return dto.Dashboard, nil
}
func (repo *fakeDashboardRepo) GetProvisionedDashboardData(name string) ([]*m.DashboardProvisioning, error) {
return repo.provisioned, nil
}
var fakeRepo *fakeDashboardRepo
// This tests two main scenarios. If a user has access to execute an action on a dashboard:
// 1. and the dashboard is in a folder which does not have an acl
// 2. and the dashboard is in a folder which does have an acl
// This tests three main scenarios.
// If a user has access to execute an action on a dashboard:
// 1. and the dashboard is in a folder which does not have an acl
// 2. and the dashboard is in a folder which does have an acl
// 3. Post dashboard response tests
func TestDashboardApiEndpoint(t *testing.T) {
Convey("Given a dashboard with a parent folder which does not have an acl", t, func() {
@@ -81,14 +61,6 @@ func TestDashboardApiEndpoint(t *testing.T) {
return nil
})
cmd := m.SaveDashboardCommand{
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"folderId": fakeDash.FolderId,
"title": fakeDash.Title,
"id": fakeDash.Id,
}),
}
// This tests two scenarios:
// 1. user is an org viewer
// 2. user is an org editor
@@ -151,11 +123,6 @@ func TestDashboardApiEndpoint(t *testing.T) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
Convey("When user is an Org Editor", func() {
@@ -216,32 +183,6 @@ func TestDashboardApiEndpoint(t *testing.T) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboardShouldReturnSuccess(sc)
})
Convey("When saving a dashboard folder in another folder", func() {
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
query.Result = fakeDash
query.Result.IsFolder = true
return nil
})
invalidCmd := m.SaveDashboardCommand{
FolderId: fakeDash.FolderId,
IsFolder: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"folderId": fakeDash.FolderId,
"title": fakeDash.Title,
}),
}
Convey("Should return an error", func() {
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, invalidCmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 400)
})
})
})
})
})
@@ -284,15 +225,6 @@ func TestDashboardApiEndpoint(t *testing.T) {
return nil
})
cmd := m.SaveDashboardCommand{
FolderId: fakeDash.FolderId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": fakeDash.Id,
"folderId": fakeDash.FolderId,
"title": fakeDash.Title,
}),
}
// This tests six scenarios:
// 1. user is an org viewer AND has no permissions for this dashboard
// 2. user is an org editor AND has no permissions for this dashboard
@@ -357,11 +289,6 @@ func TestDashboardApiEndpoint(t *testing.T) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
Convey("When user is an Org Editor and has no permissions for this dashboard", func() {
@@ -420,11 +347,6 @@ func TestDashboardApiEndpoint(t *testing.T) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
Convey("When user is an Org Viewer but has an edit permission", func() {
@@ -494,10 +416,6 @@ func TestDashboardApiEndpoint(t *testing.T) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboardShouldReturnSuccess(sc)
})
})
Convey("When user is an Org Viewer and viewers can edit", func() {
@@ -627,10 +545,6 @@ func TestDashboardApiEndpoint(t *testing.T) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 200)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboardShouldReturnSuccess(sc)
})
})
Convey("When user is an Org Editor but has a view permission", func() {
@@ -698,11 +612,6 @@ func TestDashboardApiEndpoint(t *testing.T) {
CallGetDashboardVersions(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 403)
})
})
})
@@ -736,6 +645,104 @@ func TestDashboardApiEndpoint(t *testing.T) {
})
})
})
Convey("Post dashboard response tests", t, func() {
// This tests that a valid request returns correct response
Convey("Given a correct request for creating a dashboard", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
UserId: 5,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Dash",
}),
Overwrite: true,
FolderId: 3,
IsFolder: false,
Message: "msg",
}
mock := &dashboards.FakeDashboardService{
SaveDashboardResult: &m.Dashboard{
Id: 2,
Uid: "uid",
Title: "Dash",
Slug: "dash",
Version: 2,
},
}
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", mock, cmd, func(sc *scenarioContext) {
CallPostDashboardShouldReturnSuccess(sc)
Convey("It should call dashboard service with correct data", func() {
dto := mock.SavedDashboards[0]
So(dto.OrgId, ShouldEqual, cmd.OrgId)
So(dto.User.UserId, ShouldEqual, cmd.UserId)
So(dto.Dashboard.FolderId, ShouldEqual, 3)
So(dto.Dashboard.Title, ShouldEqual, "Dash")
So(dto.Overwrite, ShouldBeTrue)
So(dto.Message, ShouldEqual, "msg")
})
Convey("It should return correct response data", func() {
result := sc.ToJson()
So(result.Get("status").MustString(), ShouldEqual, "success")
So(result.Get("id").MustInt64(), ShouldEqual, 2)
So(result.Get("uid").MustString(), ShouldEqual, "uid")
So(result.Get("slug").MustString(), ShouldEqual, "dash")
So(result.Get("url").MustString(), ShouldEqual, "/d/uid/dash")
})
})
})
// This tests that invalid requests returns expected error responses
Convey("Given incorrect requests for creating a dashboard", func() {
testCases := []struct {
SaveError error
ExpectedStatusCode int
}{
{SaveError: m.ErrDashboardNotFound, ExpectedStatusCode: 404},
{SaveError: m.ErrFolderNotFound, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardWithSameUIDExists, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardWithSameNameInFolderExists, ExpectedStatusCode: 412},
{SaveError: m.ErrDashboardVersionMismatch, ExpectedStatusCode: 412},
{SaveError: m.ErrDashboardTitleEmpty, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardFolderCannotHaveParent, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardContainsInvalidAlertData, ExpectedStatusCode: 500},
{SaveError: m.ErrDashboardFailedToUpdateAlertData, ExpectedStatusCode: 500},
{SaveError: m.ErrDashboardFailedGenerateUniqueUid, ExpectedStatusCode: 500},
{SaveError: m.ErrDashboardTypeMismatch, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardFolderWithSameNameAsDashboard, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardWithSameNameAsFolder, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardFolderNameExists, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardUpdateAccessDenied, ExpectedStatusCode: 403},
{SaveError: m.ErrDashboardInvalidUid, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardUidToLong, ExpectedStatusCode: 400},
{SaveError: m.UpdatePluginDashboardError{PluginId: "plug"}, ExpectedStatusCode: 412},
}
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "",
}),
}
for _, tc := range testCases {
mock := &dashboards.FakeDashboardService{
SaveDashboardError: tc.SaveError,
}
postDashboardScenario(fmt.Sprintf("Expect '%s' error when calling POST on", tc.SaveError.Error()), "/api/dashboards", "/api/dashboards", mock, cmd, func(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, tc.ExpectedStatusCode)
})
}
})
})
}
func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
@@ -790,19 +797,6 @@ func CallDeleteDashboardByUid(sc *scenarioContext) {
}
func CallPostDashboard(sc *scenarioContext) {
bus.AddHandler("test", func(cmd *alerting.ValidateDashboardAlertsCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error {
cmd.Result = &m.Dashboard{Id: 2, Slug: "Dash", Version: 2}
return nil
})
bus.AddHandler("test", func(cmd *alerting.UpdateDashboardAlertsCommand) error {
return nil
})
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
}
@@ -810,33 +804,29 @@ func CallPostDashboardShouldReturnSuccess(sc *scenarioContext) {
CallPostDashboard(sc)
So(sc.resp.Code, ShouldEqual, 200)
result := sc.ToJson()
So(result.Get("status").MustString(), ShouldEqual, "success")
So(result.Get("id").MustInt64(), ShouldBeGreaterThan, 0)
So(result.Get("uid").MustString(), ShouldNotBeNil)
So(result.Get("slug").MustString(), ShouldNotBeNil)
So(result.Get("url").MustString(), ShouldNotBeNil)
}
func postDashboardScenario(desc string, url string, routePattern string, role m.RoleType, cmd m.SaveDashboardCommand, fn scenarioFunc) {
func postDashboardScenario(desc string, url string, routePattern string, mock *dashboards.FakeDashboardService, cmd m.SaveDashboardCommand, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
sc.context.OrgRole = role
sc.context.SignedInUser = &m.SignedInUser{OrgId: cmd.OrgId, UserId: cmd.UserId}
return PostDashboard(c, cmd)
})
fakeRepo = &fakeDashboardRepo{}
dashboards.SetRepository(fakeRepo)
origNewDashboardService := dashboards.NewService
dashboards.MockDashboardService(mock)
sc.m.Post(routePattern, sc.defaultHandler)
defer func() {
dashboards.NewService = origNewDashboardService
}()
fn(sc)
})
}

View File

@@ -168,7 +168,7 @@ func ImportDashboard(c *middleware.Context, apiCmd dtos.ImportDashboardCommand)
cmd := plugins.ImportDashboardCommand{
OrgId: c.OrgId,
UserId: c.UserId,
User: c.SignedInUser,
PluginId: apiCmd.PluginId,
Path: apiCmd.Path,
Inputs: apiCmd.Inputs,

View File

@@ -210,3 +210,17 @@ type AlertStateInfoDTO struct {
State AlertStateType `json:"state"`
NewStateDate time.Time `json:"newStateDate"`
}
// "Internal" commands
type UpdateDashboardAlertsCommand struct {
UserId int64
OrgId int64
Dashboard *Dashboard
}
type ValidateDashboardAlertsCommand struct {
UserId int64
OrgId int64
Dashboard *Dashboard
}

View File

@@ -13,22 +13,26 @@ import (
// Typed errors
var (
ErrDashboardNotFound = errors.New("Dashboard not found")
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists")
ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists")
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
ErrDashboardExistingCannotChangeToDashboard = errors.New("An existing folder cannot be changed to a dashboard")
ErrDashboardTypeMismatch = errors.New("Dashboard cannot be changed to a folder")
ErrDashboardFolderWithSameNameAsDashboard = errors.New("Folder name cannot be the same as one of its dashboards")
ErrDashboardWithSameNameAsFolder = errors.New("Dashboard name cannot be the same as folder")
RootFolderName = "General"
ErrDashboardNotFound = errors.New("Dashboard not found")
ErrFolderNotFound = errors.New("Folder not found")
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists")
ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists")
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
ErrDashboardTypeMismatch = errors.New("Dashboard cannot be changed to a folder")
ErrDashboardFolderWithSameNameAsDashboard = errors.New("Folder name cannot be the same as one of its dashboards")
ErrDashboardWithSameNameAsFolder = errors.New("Dashboard name cannot be the same as folder")
ErrDashboardFolderNameExists = errors.New("A folder with that name already exists")
ErrDashboardUpdateAccessDenied = errors.New("Access denied to save dashboard")
ErrDashboardInvalidUid = errors.New("uid contains illegal characters")
ErrDashboardUidToLong = errors.New("uid to long. max 40 characters")
RootFolderName = "General"
)
type UpdatePluginDashboardError struct {
@@ -74,6 +78,25 @@ func (d *Dashboard) SetId(id int64) {
d.Data.Set("id", id)
}
func (d *Dashboard) SetUid(uid string) {
d.Uid = uid
d.Data.Set("uid", uid)
}
func (d *Dashboard) SetVersion(version int) {
d.Version = version
d.Data.Set("version", version)
}
// GetDashboardIdForSavePermissionCheck return the dashboard id to be used for checking permission of dashboard
func (d *Dashboard) GetDashboardIdForSavePermissionCheck() int64 {
if d.Id == 0 {
return d.FolderId
}
return d.Id
}
// NewDashboard creates a new dashboard
func NewDashboard(title string) *Dashboard {
dash := &Dashboard{}
@@ -92,6 +115,7 @@ func NewDashboardFolder(title string) *Dashboard {
folder.Data.Set("schemaVersion", 16)
folder.Data.Set("editable", true)
folder.Data.Set("hideControls", true)
folder.IsFolder = true
return folder
}
@@ -244,6 +268,12 @@ type DeleteDashboardCommand struct {
OrgId int64
}
type ValidateDashboardBeforeSaveCommand struct {
OrgId int64
Dashboard *Dashboard
Overwrite bool
}
//
// QUERIES
//

View File

@@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
)
type ImportDashboardCommand struct {
@@ -17,7 +18,7 @@ type ImportDashboardCommand struct {
Overwrite bool
OrgId int64
UserId int64
User *m.SignedInUser
PluginId string
Result *PluginDashboardInfoDTO
}
@@ -66,23 +67,32 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
saveCmd := m.SaveDashboardCommand{
Dashboard: generatedDash,
OrgId: cmd.OrgId,
UserId: cmd.UserId,
UserId: cmd.User.UserId,
Overwrite: cmd.Overwrite,
PluginId: cmd.PluginId,
FolderId: dashboard.FolderId,
}
if err := bus.Dispatch(&saveCmd); err != nil {
dto := &dashboards.SaveDashboardDTO{
OrgId: cmd.OrgId,
Dashboard: saveCmd.GetDashboardModel(),
Overwrite: saveCmd.Overwrite,
User: cmd.User,
}
savedDash, err := dashboards.NewService().SaveDashboard(dto)
if err != nil {
return err
}
cmd.Result = &PluginDashboardInfoDTO{
PluginId: cmd.PluginId,
Title: dashboard.Title,
Title: savedDash.Title,
Path: cmd.Path,
Revision: dashboard.Data.Get("revision").MustInt64(1),
ImportedUri: "db/" + saveCmd.Result.Slug,
ImportedUrl: saveCmd.Result.GetUrl(),
Revision: savedDash.Data.Get("revision").MustInt64(1),
ImportedUri: "db/" + savedDash.Slug,
ImportedUrl: savedDash.GetUrl(),
ImportedRevision: dashboard.Data.Get("revision").MustInt64(1),
Imported: true,
}

View File

@@ -5,9 +5,9 @@ import (
"io/ioutil"
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/ini.v1"
@@ -15,19 +15,15 @@ import (
func TestDashboardImport(t *testing.T) {
pluginScenario("When importing a plugin dashboard", t, func() {
var importedDash *m.Dashboard
bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error {
importedDash = cmd.GetDashboardModel()
cmd.Result = importedDash
return nil
})
origNewDashboardService := dashboards.NewService
mock := &dashboards.FakeDashboardService{}
dashboards.MockDashboardService(mock)
cmd := ImportDashboardCommand{
PluginId: "test-app",
Path: "dashboards/connections.json",
OrgId: 1,
UserId: 1,
User: &m.SignedInUser{UserId: 1, OrgRole: m.ROLE_ADMIN},
Inputs: []ImportDashboardInput{
{Name: "*", Type: "datasource", Value: "graphite"},
},
@@ -37,18 +33,22 @@ func TestDashboardImport(t *testing.T) {
So(err, ShouldBeNil)
Convey("should install dashboard", func() {
So(importedDash, ShouldNotBeNil)
So(cmd.Result, ShouldNotBeNil)
resultStr, _ := importedDash.Data.EncodePretty()
resultStr, _ := mock.SavedDashboards[0].Dashboard.Data.EncodePretty()
expectedBytes, _ := ioutil.ReadFile("../../tests/test-app/dashboards/connections_result.json")
expectedJson, _ := simplejson.NewJson(expectedBytes)
expectedStr, _ := expectedJson.EncodePretty()
So(string(resultStr), ShouldEqual, string(expectedStr))
panel := importedDash.Data.Get("rows").GetIndex(0).Get("panels").GetIndex(0)
panel := mock.SavedDashboards[0].Dashboard.Data.Get("rows").GetIndex(0).Get("panels").GetIndex(0)
So(panel.Get("datasource").MustString(), ShouldEqual, "graphite")
})
Reset(func() {
dashboards.NewService = origNewDashboardService
})
})
Convey("When evaling dashboard template", t, func() {
@@ -84,7 +84,6 @@ func TestDashboardImport(t *testing.T) {
})
})
}
func pluginScenario(desc string, t *testing.T, fn func()) {

View File

@@ -47,7 +47,7 @@ func autoUpdateAppDashboard(pluginDashInfo *PluginDashboardInfoDTO, orgId int64)
PluginId: pluginDashInfo.PluginId,
Overwrite: true,
Dashboard: dash.Data,
UserId: 0,
User: &m.SignedInUser{UserId: 0, OrgRole: m.ROLE_ADMIN},
Path: pluginDashInfo.Path,
}

View File

@@ -5,24 +5,12 @@ import (
m "github.com/grafana/grafana/pkg/models"
)
type UpdateDashboardAlertsCommand struct {
UserId int64
OrgId int64
Dashboard *m.Dashboard
}
type ValidateDashboardAlertsCommand struct {
UserId int64
OrgId int64
Dashboard *m.Dashboard
}
func init() {
bus.AddHandler("alerting", updateDashboardAlerts)
bus.AddHandler("alerting", validateDashboardAlerts)
}
func validateDashboardAlerts(cmd *ValidateDashboardAlertsCommand) error {
func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error {
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
if _, err := extractor.GetAlerts(); err != nil {
@@ -32,7 +20,7 @@ func validateDashboardAlerts(cmd *ValidateDashboardAlertsCommand) error {
return nil
}
func updateDashboardAlerts(cmd *UpdateDashboardAlertsCommand) error {
func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error {
saveAlerts := m.SaveAlertsCommand{
OrgId: cmd.OrgId,
UserId: cmd.UserId,

View File

@@ -0,0 +1,231 @@
package dashboards
import (
"strings"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/util"
)
// DashboardService service for operating on dashboards
type DashboardService interface {
SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error)
}
// DashboardProvisioningService service for operating on provisioned dashboards
type DashboardProvisioningService interface {
SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
SaveFolderForProvisionedDashboards(*SaveDashboardDTO) (*models.Dashboard, error)
GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
}
// NewService factory for creating a new dashboard service
var NewService = func() DashboardService {
return &dashboardServiceImpl{}
}
// NewProvisioningService factory for creating a new dashboard provisioning service
var NewProvisioningService = func() DashboardProvisioningService {
return &dashboardServiceImpl{}
}
type SaveDashboardDTO struct {
OrgId int64
UpdatedAt time.Time
User *models.SignedInUser
Message string
Overwrite bool
Dashboard *models.Dashboard
}
type dashboardServiceImpl struct{}
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
}
func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO) (*models.SaveDashboardCommand, error) {
dash := dto.Dashboard
dash.Title = strings.TrimSpace(dash.Title)
dash.Data.Set("title", dash.Title)
dash.SetUid(strings.TrimSpace(dash.Uid))
if dash.Title == "" {
return nil, models.ErrDashboardTitleEmpty
}
if dash.IsFolder && dash.FolderId > 0 {
return nil, models.ErrDashboardFolderCannotHaveParent
}
if dash.IsFolder && strings.ToLower(dash.Title) == strings.ToLower(models.RootFolderName) {
return nil, models.ErrDashboardFolderNameExists
}
if !util.IsValidShortUid(dash.Uid) {
return nil, models.ErrDashboardInvalidUid
} else if len(dash.Uid) > 40 {
return nil, models.ErrDashboardUidToLong
}
validateAlertsCmd := models.ValidateDashboardAlertsCommand{
OrgId: dto.OrgId,
Dashboard: dash,
}
if err := bus.Dispatch(&validateAlertsCmd); err != nil {
return nil, models.ErrDashboardContainsInvalidAlertData
}
validateBeforeSaveCmd := models.ValidateDashboardBeforeSaveCommand{
OrgId: dto.OrgId,
Dashboard: dash,
Overwrite: dto.Overwrite,
}
if err := bus.Dispatch(&validateBeforeSaveCmd); err != nil {
return nil, err
}
guard := guardian.New(dash.GetDashboardIdForSavePermissionCheck(), dto.OrgId, dto.User)
if canSave, err := guard.CanSave(); err != nil || !canSave {
if err != nil {
return nil, err
}
return nil, models.ErrDashboardUpdateAccessDenied
}
cmd := &models.SaveDashboardCommand{
Dashboard: dash.Data,
Message: dto.Message,
OrgId: dto.OrgId,
Overwrite: dto.Overwrite,
UserId: dto.User.UserId,
FolderId: dash.FolderId,
IsFolder: dash.IsFolder,
}
if !dto.UpdatedAt.IsZero() {
cmd.UpdatedAt = dto.UpdatedAt
}
return cmd, nil
}
func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand, dto *SaveDashboardDTO) error {
alertCmd := models.UpdateDashboardAlertsCommand{
OrgId: dto.OrgId,
UserId: dto.User.UserId,
Dashboard: cmd.Result,
}
if err := bus.Dispatch(&alertCmd); err != nil {
return models.ErrDashboardFailedToUpdateAlertData
}
return nil
}
func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
dto.User = &models.SignedInUser{
UserId: 0,
OrgRole: models.ROLE_ADMIN,
}
cmd, err := dr.buildSaveDashboardCommand(dto)
if err != nil {
return nil, err
}
saveCmd := &models.SaveProvisionedDashboardCommand{
DashboardCmd: cmd,
DashboardProvisioning: provisioning,
}
// dashboard
err = bus.Dispatch(saveCmd)
if err != nil {
return nil, err
}
//alerts
err = dr.updateAlerting(cmd, dto)
if err != nil {
return nil, err
}
return cmd.Result, nil
}
func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDashboardDTO) (*models.Dashboard, error) {
dto.User = &models.SignedInUser{
UserId: 0,
OrgRole: models.ROLE_ADMIN,
}
cmd, err := dr.buildSaveDashboardCommand(dto)
if err != nil {
return nil, err
}
err = bus.Dispatch(cmd)
if err != nil {
return nil, err
}
err = dr.updateAlerting(cmd, dto)
if err != nil {
return nil, err
}
return cmd.Result, nil
}
func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
cmd, err := dr.buildSaveDashboardCommand(dto)
if err != nil {
return nil, err
}
err = bus.Dispatch(cmd)
if err != nil {
return nil, err
}
err = dr.updateAlerting(cmd, dto)
if err != nil {
return nil, err
}
return cmd.Result, nil
}
type FakeDashboardService struct {
SaveDashboardResult *models.Dashboard
SaveDashboardError error
SavedDashboards []*SaveDashboardDTO
}
func (s *FakeDashboardService) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
s.SavedDashboards = append(s.SavedDashboards, dto)
if s.SaveDashboardResult == nil && s.SaveDashboardError == nil {
s.SaveDashboardResult = dto.Dashboard
}
return s.SaveDashboardResult, s.SaveDashboardError
}
func MockDashboardService(mock *FakeDashboardService) {
NewService = func() DashboardService {
return mock
}
}

View File

@@ -0,0 +1,144 @@
package dashboards
import (
"errors"
"testing"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestDashboardService(t *testing.T) {
Convey("Dashboard service tests", t, func() {
service := dashboardServiceImpl{}
origNewDashboardGuardian := guardian.New
mockDashboardGuardian(&fakeDashboardGuardian{canSave: true})
Convey("Save dashboard validation", func() {
dto := &SaveDashboardDTO{}
Convey("When saving a dashboard with empty title it should return error", func() {
titles := []string{"", " ", " \t "}
for _, title := range titles {
dto.Dashboard = models.NewDashboard(title)
_, err := service.SaveDashboard(dto)
So(err, ShouldEqual, models.ErrDashboardTitleEmpty)
}
})
Convey("Should return validation error if it's a folder and have a folder id", func() {
dto.Dashboard = models.NewDashboardFolder("Folder")
dto.Dashboard.FolderId = 1
_, err := service.SaveDashboard(dto)
So(err, ShouldEqual, models.ErrDashboardFolderCannotHaveParent)
})
Convey("Should return validation error if folder is named General", func() {
dto.Dashboard = models.NewDashboardFolder("General")
_, err := service.SaveDashboard(dto)
So(err, ShouldEqual, models.ErrDashboardFolderNameExists)
})
Convey("When saving a dashboard should validate uid", func() {
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
return nil
})
testCases := []struct {
Uid string
Error error
}{
{Uid: "", Error: nil},
{Uid: " ", Error: nil},
{Uid: " \t ", Error: nil},
{Uid: "asdf90_-", Error: nil},
{Uid: "asdf/90", Error: models.ErrDashboardInvalidUid},
{Uid: " asdfghjklqwertyuiopzxcvbnmasdfghjklqwer ", Error: nil},
{Uid: "asdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnm", Error: models.ErrDashboardUidToLong},
}
for _, tc := range testCases {
dto.Dashboard = models.NewDashboard("title")
dto.Dashboard.SetUid(tc.Uid)
dto.User = &models.SignedInUser{}
_, err := service.buildSaveDashboardCommand(dto)
So(err, ShouldEqual, tc.Error)
}
})
Convey("Should return validation error if alert data is invalid", func() {
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return errors.New("error")
})
dto.Dashboard = models.NewDashboard("Dash")
_, err := service.SaveDashboard(dto)
So(err, ShouldEqual, models.ErrDashboardContainsInvalidAlertData)
})
})
Reset(func() {
guardian.New = origNewDashboardGuardian
})
})
}
func mockDashboardGuardian(mock *fakeDashboardGuardian) {
guardian.New = func(dashId int64, orgId int64, user *models.SignedInUser) guardian.DashboardGuardian {
mock.orgId = orgId
mock.dashId = dashId
mock.user = user
return mock
}
}
type fakeDashboardGuardian struct {
dashId int64
orgId int64
user *models.SignedInUser
canSave bool
canEdit bool
canView bool
canAdmin bool
hasPermission bool
checkPermissionBeforeUpdate bool
}
func (g *fakeDashboardGuardian) CanSave() (bool, error) {
return g.canSave, nil
}
func (g *fakeDashboardGuardian) CanEdit() (bool, error) {
return g.canEdit, nil
}
func (g *fakeDashboardGuardian) CanView() (bool, error) {
return g.canView, nil
}
func (g *fakeDashboardGuardian) CanAdmin() (bool, error) {
return g.canAdmin, nil
}
func (g *fakeDashboardGuardian) HasPermission(permission models.PermissionType) (bool, error) {
return g.hasPermission, nil
}
func (g *fakeDashboardGuardian) CheckPermissionBeforeUpdate(permission models.PermissionType, updatePermissions []*models.DashboardAcl) (bool, error) {
return g.checkPermissionBeforeUpdate, nil
}
func (g *fakeDashboardGuardian) GetAcl() ([]*models.DashboardAclInfoDTO, error) {
return nil, nil
}

View File

@@ -1,143 +0,0 @@
package dashboards
import (
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/util"
)
type Repository interface {
SaveDashboard(*SaveDashboardDTO) (*models.Dashboard, error)
SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
}
var repositoryInstance Repository
func GetRepository() Repository {
return repositoryInstance
}
func SetRepository(rep Repository) {
repositoryInstance = rep
}
type SaveDashboardDTO struct {
OrgId int64
UpdatedAt time.Time
UserId int64
Message string
Overwrite bool
Dashboard *models.Dashboard
}
type DashboardRepository struct{}
func (dr *DashboardRepository) 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
}
func (dr *DashboardRepository) buildSaveDashboardCommand(dto *SaveDashboardDTO) (*models.SaveDashboardCommand, error) {
dashboard := dto.Dashboard
if dashboard.Title == "" {
return nil, models.ErrDashboardTitleEmpty
}
if err := util.VerifyUid(dashboard.Uid); err != nil {
return nil, err
}
validateAlertsCmd := alerting.ValidateDashboardAlertsCommand{
OrgId: dto.OrgId,
Dashboard: dashboard,
}
if err := bus.Dispatch(&validateAlertsCmd); err != nil {
return nil, models.ErrDashboardContainsInvalidAlertData
}
cmd := &models.SaveDashboardCommand{
Dashboard: dashboard.Data,
Message: dto.Message,
OrgId: dto.OrgId,
Overwrite: dto.Overwrite,
UserId: dto.UserId,
FolderId: dashboard.FolderId,
IsFolder: dashboard.IsFolder,
}
if !dto.UpdatedAt.IsZero() {
cmd.UpdatedAt = dto.UpdatedAt
}
return cmd, nil
}
func (dr *DashboardRepository) updateAlerting(cmd *models.SaveDashboardCommand, dto *SaveDashboardDTO) error {
alertCmd := alerting.UpdateDashboardAlertsCommand{
OrgId: dto.OrgId,
UserId: dto.UserId,
Dashboard: cmd.Result,
}
if err := bus.Dispatch(&alertCmd); err != nil {
return models.ErrDashboardFailedToUpdateAlertData
}
return nil
}
func (dr *DashboardRepository) SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
cmd, err := dr.buildSaveDashboardCommand(dto)
if err != nil {
return nil, err
}
saveCmd := &models.SaveProvisionedDashboardCommand{
DashboardCmd: cmd,
DashboardProvisioning: provisioning,
}
// dashboard
err = bus.Dispatch(saveCmd)
if err != nil {
return nil, err
}
//alerts
err = dr.updateAlerting(cmd, dto)
if err != nil {
return nil, err
}
return cmd.Result, nil
}
func (dr *DashboardRepository) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
cmd, err := dr.buildSaveDashboardCommand(dto)
if err != nil {
return nil, err
}
err = bus.Dispatch(cmd)
if err != nil {
return nil, err
}
err = dr.updateAlerting(cmd, dto)
if err != nil {
return nil, err
}
return cmd.Result, nil
}

View File

@@ -1,43 +0,0 @@
package dashboards
import (
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/util"
)
func TestDashboardsService(t *testing.T) {
bus.ClearBusHandlers()
bus.AddHandler("test", func(cmd *alerting.ValidateDashboardAlertsCommand) error {
return nil
})
testCases := []struct {
Uid string
Error error
}{
{Uid: "", Error: nil},
{Uid: "asdf90_-", Error: nil},
{Uid: "asdf/90", Error: util.ErrDashboardInvalidUid},
{Uid: "asdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnm", Error: util.ErrDashboardUidToLong},
}
repo := &DashboardRepository{}
for _, tc := range testCases {
dto := &SaveDashboardDTO{
Dashboard: &models.Dashboard{Title: "title", Uid: tc.Uid},
}
_, err := repo.buildSaveDashboardCommand(dto)
if err != tc.Error {
t.Fatalf("expected %s to return %v", tc.Uid, tc.Error)
}
}
}

View File

@@ -7,7 +7,18 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
type DashboardGuardian struct {
// DashboardGuardian to be used for guard against operations without access on dashboard and acl
type DashboardGuardian interface {
CanSave() (bool, error)
CanEdit() (bool, error)
CanView() (bool, error)
CanAdmin() (bool, error)
HasPermission(permission m.PermissionType) (bool, error)
CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error)
GetAcl() ([]*m.DashboardAclInfoDTO, error)
}
type dashboardGuardianImpl struct {
user *m.SignedInUser
dashId int64
orgId int64
@@ -16,8 +27,9 @@ type DashboardGuardian struct {
log log.Logger
}
func NewDashboardGuardian(dashId int64, orgId int64, user *m.SignedInUser) *DashboardGuardian {
return &DashboardGuardian{
// New factory for creating a new dashboard guardian instance
var New = func(dashId int64, orgId int64, user *m.SignedInUser) DashboardGuardian {
return &dashboardGuardianImpl{
user: user,
dashId: dashId,
orgId: orgId,
@@ -25,11 +37,11 @@ func NewDashboardGuardian(dashId int64, orgId int64, user *m.SignedInUser) *Dash
}
}
func (g *DashboardGuardian) CanSave() (bool, error) {
func (g *dashboardGuardianImpl) CanSave() (bool, error) {
return g.HasPermission(m.PERMISSION_EDIT)
}
func (g *DashboardGuardian) CanEdit() (bool, error) {
func (g *dashboardGuardianImpl) CanEdit() (bool, error) {
if setting.ViewersCanEdit {
return g.HasPermission(m.PERMISSION_VIEW)
}
@@ -37,15 +49,15 @@ func (g *DashboardGuardian) CanEdit() (bool, error) {
return g.HasPermission(m.PERMISSION_EDIT)
}
func (g *DashboardGuardian) CanView() (bool, error) {
func (g *dashboardGuardianImpl) CanView() (bool, error) {
return g.HasPermission(m.PERMISSION_VIEW)
}
func (g *DashboardGuardian) CanAdmin() (bool, error) {
func (g *dashboardGuardianImpl) CanAdmin() (bool, error) {
return g.HasPermission(m.PERMISSION_ADMIN)
}
func (g *DashboardGuardian) HasPermission(permission m.PermissionType) (bool, error) {
func (g *dashboardGuardianImpl) HasPermission(permission m.PermissionType) (bool, error) {
if g.user.OrgRole == m.ROLE_ADMIN {
return true, nil
}
@@ -58,7 +70,7 @@ func (g *DashboardGuardian) HasPermission(permission m.PermissionType) (bool, er
return g.checkAcl(permission, acl)
}
func (g *DashboardGuardian) checkAcl(permission m.PermissionType, acl []*m.DashboardAclInfoDTO) (bool, error) {
func (g *dashboardGuardianImpl) checkAcl(permission m.PermissionType, acl []*m.DashboardAclInfoDTO) (bool, error) {
orgRole := g.user.OrgRole
teamAclItems := []*m.DashboardAclInfoDTO{}
@@ -106,7 +118,7 @@ func (g *DashboardGuardian) checkAcl(permission m.PermissionType, acl []*m.Dashb
return false, nil
}
func (g *DashboardGuardian) CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error) {
func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error) {
if g.user.OrgRole == m.ROLE_ADMIN {
return true, nil
}
@@ -121,7 +133,7 @@ func (g *DashboardGuardian) CheckPermissionBeforeUpdate(permission m.PermissionT
}
// GetAcl returns dashboard acl
func (g *DashboardGuardian) GetAcl() ([]*m.DashboardAclInfoDTO, error) {
func (g *dashboardGuardianImpl) GetAcl() ([]*m.DashboardAclInfoDTO, error) {
if g.acl != nil {
return g.acl, nil
}
@@ -135,7 +147,7 @@ func (g *DashboardGuardian) GetAcl() ([]*m.DashboardAclInfoDTO, error) {
return g.acl, nil
}
func (g *DashboardGuardian) getTeams() ([]*m.Team, error) {
func (g *dashboardGuardianImpl) getTeams() ([]*m.Team, error) {
if g.groups != nil {
return g.groups, nil
}

View File

@@ -25,10 +25,10 @@ var (
)
type fileReader struct {
Cfg *DashboardsAsConfig
Path string
log log.Logger
dashboardRepo dashboards.Repository
Cfg *DashboardsAsConfig
Path string
log log.Logger
dashboardService dashboards.DashboardProvisioningService
}
func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) {
@@ -48,10 +48,10 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade
}
return &fileReader{
Cfg: cfg,
Path: path,
log: log,
dashboardRepo: dashboards.GetRepository(),
Cfg: cfg,
Path: path,
log: log,
dashboardService: dashboards.NewProvisioningService(),
}, nil
}
@@ -89,12 +89,12 @@ func (fr *fileReader) startWalkingDisk() error {
}
}
folderId, err := getOrCreateFolderId(fr.Cfg, fr.dashboardRepo)
folderId, err := getOrCreateFolderId(fr.Cfg, fr.dashboardService)
if err != nil && err != ErrFolderNameMissing {
return err
}
provisionedDashboardRefs, err := getProvisionedDashboardByPath(fr.dashboardRepo, fr.Cfg.Name)
provisionedDashboardRefs, err := getProvisionedDashboardByPath(fr.dashboardService, fr.Cfg.Name)
if err != nil {
return err
}
@@ -180,12 +180,12 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil
fr.log.Debug("saving new dashboard", "file", path)
dp := &models.DashboardProvisioning{ExternalId: path, Name: fr.Cfg.Name, Updated: resolvedFileInfo.ModTime().Unix()}
_, err = fr.dashboardRepo.SaveProvisionedDashboard(dash, dp)
_, err = fr.dashboardService.SaveProvisionedDashboard(dash, dp)
return provisioningMetadata, err
}
func getProvisionedDashboardByPath(repo dashboards.Repository, name string) (map[string]*models.DashboardProvisioning, error) {
arr, err := repo.GetProvisionedDashboardData(name)
func getProvisionedDashboardByPath(service dashboards.DashboardProvisioningService, name string) (map[string]*models.DashboardProvisioning, error) {
arr, err := service.GetProvisionedDashboardData(name)
if err != nil {
return nil, err
}
@@ -198,7 +198,7 @@ func getProvisionedDashboardByPath(repo dashboards.Repository, name string) (map
return byPath, nil
}
func getOrCreateFolderId(cfg *DashboardsAsConfig, repo dashboards.Repository) (int64, error) {
func getOrCreateFolderId(cfg *DashboardsAsConfig, service dashboards.DashboardProvisioningService) (int64, error) {
if cfg.Folder == "" {
return 0, ErrFolderNameMissing
}
@@ -213,11 +213,11 @@ func getOrCreateFolderId(cfg *DashboardsAsConfig, repo dashboards.Repository) (i
// dashboard folder not found. create one.
if err == models.ErrDashboardNotFound {
dash := &dashboards.SaveDashboardDTO{}
dash.Dashboard = models.NewDashboard(cfg.Folder)
dash.Dashboard = models.NewDashboardFolder(cfg.Folder)
dash.Dashboard.IsFolder = true
dash.Overwrite = true
dash.OrgId = cfg.OrgId
dbDash, err := repo.SaveDashboard(dash)
dbDash, err := service.SaveFolderForProvisionedDashboards(dash)
if err != nil {
return 0, err
}

View File

@@ -19,16 +19,16 @@ var (
brokenDashboards string = "./test-dashboards/broken-dashboards"
oneDashboard string = "./test-dashboards/one-dashboard"
fakeRepo *fakeDashboardRepo
fakeService *fakeDashboardProvisioningService
)
func TestDashboardFileReader(t *testing.T) {
Convey("Dashboard file reader", t, func() {
bus.ClearBusHandlers()
fakeRepo = &fakeDashboardRepo{}
origNewDashboardProvisioningService := dashboards.NewProvisioningService
fakeService = mockDashboardProvisioningService()
bus.AddHandler("test", mockGetDashboardQuery)
dashboards.SetRepository(fakeRepo)
logger := log.New("test.logger")
Convey("Reading dashboards from disk", func() {
@@ -54,7 +54,7 @@ func TestDashboardFileReader(t *testing.T) {
folders := 0
dashboards := 0
for _, i := range fakeRepo.inserted {
for _, i := range fakeService.inserted {
if i.Dashboard.IsFolder {
folders++
} else {
@@ -71,7 +71,7 @@ func TestDashboardFileReader(t *testing.T) {
stat, _ := os.Stat(oneDashboard + "/dashboard1.json")
fakeRepo.getDashboard = append(fakeRepo.getDashboard, &models.Dashboard{
fakeService.getDashboard = append(fakeService.getDashboard, &models.Dashboard{
Updated: stat.ModTime().AddDate(0, 0, -1),
Slug: "grafana",
})
@@ -82,7 +82,7 @@ func TestDashboardFileReader(t *testing.T) {
err = reader.startWalkingDisk()
So(err, ShouldBeNil)
So(len(fakeRepo.inserted), ShouldEqual, 1)
So(len(fakeService.inserted), ShouldEqual, 1)
})
Convey("Invalid configuration should return error", func() {
@@ -116,7 +116,7 @@ func TestDashboardFileReader(t *testing.T) {
},
}
_, err := getOrCreateFolderId(cfg, fakeRepo)
_, err := getOrCreateFolderId(cfg, fakeService)
So(err, ShouldEqual, ErrFolderNameMissing)
})
@@ -131,15 +131,15 @@ func TestDashboardFileReader(t *testing.T) {
},
}
folderId, err := getOrCreateFolderId(cfg, fakeRepo)
folderId, err := getOrCreateFolderId(cfg, fakeService)
So(err, ShouldBeNil)
inserted := false
for _, d := range fakeRepo.inserted {
for _, d := range fakeService.inserted {
if d.Dashboard.IsFolder && d.Dashboard.Id == folderId {
inserted = true
}
}
So(len(fakeRepo.inserted), ShouldEqual, 1)
So(len(fakeService.inserted), ShouldEqual, 1)
So(inserted, ShouldBeTrue)
})
@@ -180,6 +180,10 @@ func TestDashboardFileReader(t *testing.T) {
So(reader.Path, ShouldEqual, defaultDashboards)
})
})
Reset(func() {
dashboards.NewProvisioningService = origNewDashboardProvisioningService
})
})
}
@@ -212,29 +216,37 @@ func (ffi FakeFileInfo) Sys() interface{} {
return nil
}
type fakeDashboardRepo struct {
func mockDashboardProvisioningService() *fakeDashboardProvisioningService {
mock := fakeDashboardProvisioningService{}
dashboards.NewProvisioningService = func() dashboards.DashboardProvisioningService {
return &mock
}
return &mock
}
type fakeDashboardProvisioningService struct {
inserted []*dashboards.SaveDashboardDTO
provisioned []*models.DashboardProvisioning
getDashboard []*models.Dashboard
}
func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardDTO) (*models.Dashboard, error) {
repo.inserted = append(repo.inserted, json)
return json.Dashboard, nil
func (s *fakeDashboardProvisioningService) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
return s.provisioned, nil
}
func (repo *fakeDashboardRepo) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
return repo.provisioned, nil
func (s *fakeDashboardProvisioningService) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
s.inserted = append(s.inserted, dto)
s.provisioned = append(s.provisioned, provisioning)
return dto.Dashboard, nil
}
func (repo *fakeDashboardRepo) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
repo.inserted = append(repo.inserted, dto)
repo.provisioned = append(repo.provisioned, provisioning)
func (s *fakeDashboardProvisioningService) SaveFolderForProvisionedDashboards(dto *dashboards.SaveDashboardDTO) (*models.Dashboard, error) {
s.inserted = append(s.inserted, dto)
return dto.Dashboard, nil
}
func mockGetDashboardQuery(cmd *models.GetDashboardQuery) error {
for _, d := range fakeRepo.getDashboard {
for _, d := range fakeService.getDashboard {
if d.Slug == cmd.Slug {
cmd.Result = d
return nil

View File

@@ -23,6 +23,7 @@ func init() {
bus.AddHandler("sql", GetDashboardsByPluginId)
bus.AddHandler("sql", GetDashboardPermissionsForUser)
bus.AddHandler("sql", GetDashboardsBySlug)
bus.AddHandler("sql", ValidateDashboardBeforeSave)
}
var generateNewUid func() string = util.GenerateShortUid
@@ -36,38 +37,29 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
dash := cmd.GetDashboardModel()
if err := getExistingDashboardForUpdate(sess, dash, cmd); err != nil {
return err
}
var existingByTitleAndFolder m.Dashboard
dashWithTitleAndFolderExists, err := sess.Where("org_id=? AND slug=? AND (is_folder=? OR folder_id=?)", dash.OrgId, dash.Slug, dialect.BooleanStr(true), dash.FolderId).Get(&existingByTitleAndFolder)
if err != nil {
return err
}
if dashWithTitleAndFolderExists {
if dash.Id != existingByTitleAndFolder.Id {
if existingByTitleAndFolder.IsFolder && !cmd.IsFolder {
return m.ErrDashboardWithSameNameAsFolder
}
if !existingByTitleAndFolder.IsFolder && cmd.IsFolder {
return m.ErrDashboardFolderWithSameNameAsDashboard
}
if dash.Id > 0 {
var existing m.Dashboard
dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing)
if err != nil {
return err
}
if !dashWithIdExists {
return m.ErrDashboardNotFound
}
// check for is someone else has written in between
if dash.Version != existing.Version {
if cmd.Overwrite {
dash.Id = existingByTitleAndFolder.Id
dash.Version = existingByTitleAndFolder.Version
if dash.Uid == "" {
dash.Uid = existingByTitleAndFolder.Uid
}
dash.SetVersion(existing.Version)
} else {
return m.ErrDashboardWithSameNameInFolderExists
return m.ErrDashboardVersionMismatch
}
}
// do not allow plugin dashboard updates without overwrite flag
if existing.PluginId != "" && cmd.Overwrite == false {
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
}
}
if dash.Uid == "" {
@@ -75,21 +67,21 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
if err != nil {
return err
}
dash.Uid = uid
dash.Data.Set("uid", uid)
dash.SetUid(uid)
}
parentVersion := dash.Version
affectedRows := int64(0)
var err error
if dash.Id == 0 {
dash.Version = 1
dash.SetVersion(1)
metrics.M_Api_Dashboard_Insert.Inc()
dash.Data.Set("version", dash.Version)
affectedRows, err = sess.Insert(dash)
} else {
dash.Version++
dash.Data.Set("version", dash.Version)
v := dash.Version
v++
dash.SetVersion(v)
if !cmd.UpdatedAt.IsZero() {
dash.Updated = cmd.UpdatedAt
@@ -145,72 +137,6 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
return err
}
func getExistingDashboardForUpdate(sess *DBSession, dash *m.Dashboard, cmd *m.SaveDashboardCommand) (err error) {
dashWithIdExists := false
var existingById m.Dashboard
if dash.Id > 0 {
dashWithIdExists, err = sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existingById)
if err != nil {
return err
}
if !dashWithIdExists {
return m.ErrDashboardNotFound
}
if dash.Uid == "" {
dash.Uid = existingById.Uid
}
}
dashWithUidExists := false
var existingByUid m.Dashboard
if dash.Uid != "" {
dashWithUidExists, err = sess.Where("org_id=? AND uid=?", dash.OrgId, dash.Uid).Get(&existingByUid)
if err != nil {
return err
}
}
if !dashWithIdExists && !dashWithUidExists {
return nil
}
if dashWithIdExists && dashWithUidExists && existingById.Id != existingByUid.Id {
return m.ErrDashboardWithSameUIDExists
}
existing := existingById
if !dashWithIdExists && dashWithUidExists {
dash.Id = existingByUid.Id
existing = existingByUid
}
if (existing.IsFolder && !cmd.IsFolder) ||
(!existing.IsFolder && cmd.IsFolder) {
return m.ErrDashboardTypeMismatch
}
// check for is someone else has written in between
if dash.Version != existing.Version {
if cmd.Overwrite {
dash.Version = existing.Version
} else {
return m.ErrDashboardVersionMismatch
}
}
// do not allow plugin dashboard updates without overwrite flag
if existing.PluginId != "" && cmd.Overwrite == false {
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
}
return nil
}
func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) {
for i := 0; i < 3; i++ {
uid := generateNewUid()
@@ -238,8 +164,8 @@ func GetDashboard(query *m.GetDashboardQuery) error {
return m.ErrDashboardNotFound
}
dashboard.Data.Set("id", dashboard.Id)
dashboard.Data.Set("uid", dashboard.Uid)
dashboard.SetId(dashboard.Id)
dashboard.SetUid(dashboard.Uid)
query.Result = &dashboard
return nil
}
@@ -548,3 +474,128 @@ func GetDashboardUIDById(query *m.GetDashboardRefByIdQuery) error {
query.Result = us
return nil
}
func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDashboardBeforeSaveCommand) (err error) {
dash := cmd.Dashboard
dashWithIdExists := false
var existingById m.Dashboard
if dash.Id > 0 {
dashWithIdExists, err = sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existingById)
if err != nil {
return err
}
if !dashWithIdExists {
return m.ErrDashboardNotFound
}
if dash.Uid == "" {
dash.SetUid(existingById.Uid)
}
}
dashWithUidExists := false
var existingByUid m.Dashboard
if dash.Uid != "" {
dashWithUidExists, err = sess.Where("org_id=? AND uid=?", dash.OrgId, dash.Uid).Get(&existingByUid)
if err != nil {
return err
}
}
if dash.FolderId > 0 {
var existingFolder m.Dashboard
folderExists, folderErr := sess.Where("org_id=? AND id=? AND is_folder=?", dash.OrgId, dash.FolderId, dialect.BooleanStr(true)).Get(&existingFolder)
if folderErr != nil {
return folderErr
}
if !folderExists {
return m.ErrFolderNotFound
}
}
if !dashWithIdExists && !dashWithUidExists {
return nil
}
if dashWithIdExists && dashWithUidExists && existingById.Id != existingByUid.Id {
return m.ErrDashboardWithSameUIDExists
}
existing := existingById
if !dashWithIdExists && dashWithUidExists {
dash.SetId(existingByUid.Id)
dash.SetUid(existingByUid.Uid)
existing = existingByUid
}
if (existing.IsFolder && !dash.IsFolder) ||
(!existing.IsFolder && dash.IsFolder) {
return m.ErrDashboardTypeMismatch
}
// check for is someone else has written in between
if dash.Version != existing.Version {
if cmd.Overwrite {
dash.SetVersion(existing.Version)
} else {
return m.ErrDashboardVersionMismatch
}
}
// do not allow plugin dashboard updates without overwrite flag
if existing.PluginId != "" && cmd.Overwrite == false {
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
}
return nil
}
func getExistingDashboardByTitleAndFolder(sess *DBSession, cmd *m.ValidateDashboardBeforeSaveCommand) error {
dash := cmd.Dashboard
var existing m.Dashboard
exists, err := sess.Where("org_id=? AND slug=? AND (is_folder=? OR folder_id=?)", dash.OrgId, dash.Slug, dialect.BooleanStr(true), dash.FolderId).Get(&existing)
if err != nil {
return err
}
if exists && dash.Id != existing.Id {
if existing.IsFolder && !dash.IsFolder {
return m.ErrDashboardWithSameNameAsFolder
}
if !existing.IsFolder && dash.IsFolder {
return m.ErrDashboardFolderWithSameNameAsDashboard
}
if cmd.Overwrite {
dash.SetId(existing.Id)
dash.SetUid(existing.Uid)
dash.SetVersion(existing.Version)
} else {
return m.ErrDashboardWithSameNameInFolderExists
}
}
return nil
}
func ValidateDashboardBeforeSave(cmd *m.ValidateDashboardBeforeSaveCommand) (err error) {
return inTransaction(func(sess *DBSession) error {
if err = getExistingDashboardByIdOrUidForUpdate(sess, cmd); err != nil {
return err
}
if err = getExistingDashboardByTitleAndFolder(sess, cmd); err != nil {
return err
}
return nil
})
}

View File

@@ -0,0 +1,984 @@
package sqlstore
import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestIntegratedDashboardService(t *testing.T) {
Convey("Dashboard service integration tests", t, func() {
InitTestDB(t)
var testOrgId int64 = 1
Convey("Given saved folders and dashboards in organization A", func() {
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error {
return nil
})
savedFolder := saveTestFolder("Saved folder", testOrgId)
savedDashInFolder := saveTestDashboard("Saved dash in folder", testOrgId, savedFolder.Id)
saveTestDashboard("Other saved dash in folder", testOrgId, savedFolder.Id)
savedDashInGeneralFolder := saveTestDashboard("Saved dashboard in general folder", testOrgId, 0)
otherSavedFolder := saveTestFolder("Other saved folder", testOrgId)
Convey("Should return dashboard model", func() {
So(savedFolder.Title, ShouldEqual, "Saved folder")
So(savedFolder.Slug, ShouldEqual, "saved-folder")
So(savedFolder.Id, ShouldNotEqual, 0)
So(savedFolder.IsFolder, ShouldBeTrue)
So(savedFolder.FolderId, ShouldEqual, 0)
So(len(savedFolder.Uid), ShouldBeGreaterThan, 0)
So(savedDashInFolder.Title, ShouldEqual, "Saved dash in folder")
So(savedDashInFolder.Slug, ShouldEqual, "saved-dash-in-folder")
So(savedDashInFolder.Id, ShouldNotEqual, 0)
So(savedDashInFolder.IsFolder, ShouldBeFalse)
So(savedDashInFolder.FolderId, ShouldEqual, savedFolder.Id)
So(len(savedDashInFolder.Uid), ShouldBeGreaterThan, 0)
})
// Basic validation tests
Convey("When saving a dashboard with non-existing id", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": float64(123412321),
"title": "Expect error",
}),
}
err := callSaveWithError(cmd)
Convey("It should result in not found error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardNotFound)
})
})
// Given other organization
Convey("Given organization B", func() {
var otherOrgId int64 = 2
Convey("When saving a dashboard with id that are saved in organization A", func() {
cmd := models.SaveDashboardCommand{
OrgId: otherOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDashInFolder.Id,
"title": "Expect error",
}),
Overwrite: false,
}
err := callSaveWithError(cmd)
Convey("It should result in not found error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardNotFound)
})
})
permissionScenario("Given user has permission to save", true, func(sc *dashboardPermissionScenarioContext) {
Convey("When saving a dashboard with uid that are saved in organization A", func() {
var otherOrgId int64 = 2
cmd := models.SaveDashboardCommand{
OrgId: otherOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDashInFolder.Uid,
"title": "Dash with existing uid in other org",
}),
Overwrite: false,
}
res := callSaveWithResult(cmd)
Convey("It should create dashboard in other organization", func() {
So(res, ShouldNotBeNil)
query := models.GetDashboardQuery{OrgId: otherOrgId, Uid: savedDashInFolder.Uid}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.Id, ShouldNotEqual, savedDashInFolder.Id)
So(query.Result.Id, ShouldEqual, res.Id)
So(query.Result.OrgId, ShouldEqual, otherOrgId)
So(query.Result.Uid, ShouldEqual, savedDashInFolder.Uid)
})
})
})
})
// Given user has no permission to save
permissionScenario("Given user has no permission to save", false, func(sc *dashboardPermissionScenarioContext) {
Convey("When trying to create a new dashboard in the General folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Dash",
}),
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd)
Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
So(sc.dashboardGuardianMock.dashId, ShouldEqual, 0)
So(sc.dashboardGuardianMock.orgId, ShouldEqual, cmd.OrgId)
So(sc.dashboardGuardianMock.user.UserId, ShouldEqual, cmd.UserId)
})
})
Convey("When trying to create a new dashboard in other folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Dash",
}),
FolderId: otherSavedFolder.Id,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd)
Convey("It should call dashboard guardian with correct arguments and rsult in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
So(sc.dashboardGuardianMock.dashId, ShouldEqual, otherSavedFolder.Id)
So(sc.dashboardGuardianMock.orgId, ShouldEqual, cmd.OrgId)
So(sc.dashboardGuardianMock.user.UserId, ShouldEqual, cmd.UserId)
})
})
Convey("When trying to update a dashboard by existing id in the General folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDashInGeneralFolder.Id,
"title": "Dash",
}),
FolderId: savedDashInGeneralFolder.FolderId,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd)
Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
So(sc.dashboardGuardianMock.dashId, ShouldEqual, savedDashInGeneralFolder.Id)
So(sc.dashboardGuardianMock.orgId, ShouldEqual, cmd.OrgId)
So(sc.dashboardGuardianMock.user.UserId, ShouldEqual, cmd.UserId)
})
})
Convey("When trying to update a dashboard by existing id in other folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDashInFolder.Id,
"title": "Dash",
}),
FolderId: savedDashInFolder.FolderId,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd)
Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
So(sc.dashboardGuardianMock.dashId, ShouldEqual, savedDashInFolder.Id)
So(sc.dashboardGuardianMock.orgId, ShouldEqual, cmd.OrgId)
So(sc.dashboardGuardianMock.user.UserId, ShouldEqual, cmd.UserId)
})
})
})
// Given user has permission to save
permissionScenario("Given user has permission to save", true, func(sc *dashboardPermissionScenarioContext) {
Convey("and overwrite flag is set to false", func() {
shouldOverwrite := false
Convey("When creating a dashboard in General folder with same name as dashboard in other folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": savedDashInFolder.Title,
}),
FolderId: 0,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
So(res, ShouldNotBeNil)
Convey("It should create a new dashboard", func() {
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: res.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.Id, ShouldEqual, res.Id)
So(query.Result.FolderId, ShouldEqual, 0)
})
})
Convey("When creating a dashboard in other folder with same name as dashboard in General folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": savedDashInGeneralFolder.Title,
}),
FolderId: savedFolder.Id,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
So(res, ShouldNotBeNil)
Convey("It should create a new dashboard", func() {
So(res.Id, ShouldNotEqual, savedDashInGeneralFolder.Id)
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: res.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.FolderId, ShouldEqual, savedFolder.Id)
})
})
Convey("When creating a folder with same name as dashboard in other folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": savedDashInFolder.Title,
}),
IsFolder: true,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
So(res, ShouldNotBeNil)
Convey("It should create a new folder", func() {
So(res.Id, ShouldNotEqual, savedDashInGeneralFolder.Id)
So(res.IsFolder, ShouldBeTrue)
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: res.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.FolderId, ShouldEqual, 0)
So(query.Result.IsFolder, ShouldBeTrue)
})
})
Convey("When saving a dashboard without id and uid and unique title in folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Dash without id and uid",
}),
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
So(res, ShouldNotBeNil)
Convey("It should create a new dashboard", func() {
So(res.Id, ShouldBeGreaterThan, 0)
So(len(res.Uid), ShouldBeGreaterThan, 0)
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: res.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.Id, ShouldEqual, res.Id)
So(query.Result.Uid, ShouldEqual, res.Uid)
})
})
Convey("When saving a dashboard when dashboard id is zero ", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": 0,
"title": "Dash with zero id",
}),
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
So(res, ShouldNotBeNil)
Convey("It should create a new dashboard", func() {
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: res.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.Id, ShouldEqual, res.Id)
})
})
Convey("When saving a dashboard in non-existing folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Expect error",
}),
FolderId: 123412321,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in folder not found error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrFolderNotFound)
})
})
Convey("When updating an existing dashboard by id without current version", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDashInGeneralFolder.Id,
"title": "test dash 23",
}),
FolderId: savedFolder.Id,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in version mismatch error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardVersionMismatch)
})
})
Convey("When updating an existing dashboard by id with current version", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDashInGeneralFolder.Id,
"title": "Updated title",
"version": savedDashInGeneralFolder.Version,
}),
FolderId: savedFolder.Id,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
So(res, ShouldNotBeNil)
Convey("It should update dashboard", func() {
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: savedDashInGeneralFolder.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.Title, ShouldEqual, "Updated title")
So(query.Result.FolderId, ShouldEqual, savedFolder.Id)
So(query.Result.Version, ShouldBeGreaterThan, savedDashInGeneralFolder.Version)
})
})
Convey("When updating an existing dashboard by uid without current version", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDashInFolder.Uid,
"title": "test dash 23",
}),
FolderId: 0,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in version mismatch error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardVersionMismatch)
})
})
Convey("When updating an existing dashboard by uid with current version", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDashInFolder.Uid,
"title": "Updated title",
"version": savedDashInFolder.Version,
}),
FolderId: 0,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
So(res, ShouldNotBeNil)
Convey("It should update dashboard", func() {
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: savedDashInFolder.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.Title, ShouldEqual, "Updated title")
So(query.Result.FolderId, ShouldEqual, 0)
So(query.Result.Version, ShouldBeGreaterThan, savedDashInFolder.Version)
})
})
Convey("When creating a dashboard with same name as dashboard in other folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": savedDashInFolder.Title,
}),
FolderId: savedDashInFolder.FolderId,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in dashboard with same name in folder error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardWithSameNameInFolderExists)
})
})
Convey("When creating a dashboard with same name as dashboard in General folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": savedDashInGeneralFolder.Title,
}),
FolderId: savedDashInGeneralFolder.FolderId,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in dashboard with same name in folder error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardWithSameNameInFolderExists)
})
})
Convey("When creating a folder with same name as existing folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": savedFolder.Title,
}),
IsFolder: true,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in dashboard with same name in folder error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardWithSameNameInFolderExists)
})
})
})
Convey("and overwrite flag is set to true", func() {
shouldOverwrite := true
Convey("When updating an existing dashboard by id without current version", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDashInGeneralFolder.Id,
"title": "Updated title",
}),
FolderId: savedFolder.Id,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
So(res, ShouldNotBeNil)
Convey("It should update dashboard", func() {
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: savedDashInGeneralFolder.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.Title, ShouldEqual, "Updated title")
So(query.Result.FolderId, ShouldEqual, savedFolder.Id)
So(query.Result.Version, ShouldBeGreaterThan, savedDashInGeneralFolder.Version)
})
})
Convey("When updating an existing dashboard by uid without current version", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDashInFolder.Uid,
"title": "Updated title",
}),
FolderId: 0,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
So(res, ShouldNotBeNil)
Convey("It should update dashboard", func() {
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: savedDashInFolder.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.Title, ShouldEqual, "Updated title")
So(query.Result.FolderId, ShouldEqual, 0)
So(query.Result.Version, ShouldBeGreaterThan, savedDashInFolder.Version)
})
})
Convey("When updating uid for existing dashboard using id", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDashInFolder.Id,
"uid": "new-uid",
"title": savedDashInFolder.Title,
}),
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
Convey("It should update dashboard", func() {
So(res, ShouldNotBeNil)
So(res.Id, ShouldEqual, savedDashInFolder.Id)
So(res.Uid, ShouldEqual, "new-uid")
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: savedDashInFolder.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.Uid, ShouldEqual, "new-uid")
So(query.Result.Version, ShouldBeGreaterThan, savedDashInFolder.Version)
})
})
Convey("When updating uid to an existing uid for existing dashboard using id", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDashInFolder.Id,
"uid": savedDashInGeneralFolder.Uid,
"title": savedDashInFolder.Title,
}),
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in same uid exists error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardWithSameUIDExists)
})
})
Convey("When creating a dashboard with same name as dashboard in other folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": savedDashInFolder.Title,
}),
FolderId: savedDashInFolder.FolderId,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
Convey("It should overwrite existing dashboard", func() {
So(res, ShouldNotBeNil)
So(res.Id, ShouldEqual, savedDashInFolder.Id)
So(res.Uid, ShouldEqual, savedDashInFolder.Uid)
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: res.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.Id, ShouldEqual, res.Id)
So(query.Result.Uid, ShouldEqual, res.Uid)
})
})
Convey("When creating a dashboard with same name as dashboard in General folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": savedDashInGeneralFolder.Title,
}),
FolderId: savedDashInGeneralFolder.FolderId,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
Convey("It should overwrite existing dashboard", func() {
So(res, ShouldNotBeNil)
So(res.Id, ShouldEqual, savedDashInGeneralFolder.Id)
So(res.Uid, ShouldEqual, savedDashInGeneralFolder.Uid)
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: res.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.Id, ShouldEqual, res.Id)
So(query.Result.Uid, ShouldEqual, res.Uid)
})
})
Convey("When trying to update existing folder to a dashboard using id", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedFolder.Id,
"title": "new title",
}),
IsFolder: false,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in type mismatch error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardTypeMismatch)
})
})
Convey("When trying to update existing dashboard to a folder using id", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDashInFolder.Id,
"title": "new folder title",
}),
IsFolder: true,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in type mismatch error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardTypeMismatch)
})
})
Convey("When trying to update existing folder to a dashboard using uid", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedFolder.Uid,
"title": "new title",
}),
IsFolder: false,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in type mismatch error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardTypeMismatch)
})
})
Convey("When trying to update existing dashboard to a folder using uid", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDashInFolder.Uid,
"title": "new folder title",
}),
IsFolder: true,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in type mismatch error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardTypeMismatch)
})
})
Convey("When trying to update existing folder to a dashboard using title", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": savedFolder.Title,
}),
IsFolder: false,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in dashboard with same name as folder error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardWithSameNameAsFolder)
})
})
Convey("When trying to update existing dashboard to a folder using title", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": savedDashInGeneralFolder.Title,
}),
IsFolder: true,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in folder with same name as dashboard error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardFolderWithSameNameAsDashboard)
})
})
})
})
})
})
}
func mockDashboardGuardian(mock *mockDashboardGuarder) {
guardian.New = func(dashId int64, orgId int64, user *models.SignedInUser) guardian.DashboardGuardian {
mock.orgId = orgId
mock.dashId = dashId
mock.user = user
return mock
}
}
type mockDashboardGuarder struct {
dashId int64
orgId int64
user *models.SignedInUser
canSave bool
canSaveCallCounter int
canEdit bool
canView bool
canAdmin bool
hasPermission bool
checkPermissionBeforeRemove bool
checkPermissionBeforeUpdate bool
}
func (g *mockDashboardGuarder) CanSave() (bool, error) {
g.canSaveCallCounter++
return g.canSave, nil
}
func (g *mockDashboardGuarder) CanEdit() (bool, error) {
return g.canEdit, nil
}
func (g *mockDashboardGuarder) CanView() (bool, error) {
return g.canView, nil
}
func (g *mockDashboardGuarder) CanAdmin() (bool, error) {
return g.canAdmin, nil
}
func (g *mockDashboardGuarder) HasPermission(permission models.PermissionType) (bool, error) {
return g.hasPermission, nil
}
func (g *mockDashboardGuarder) CheckPermissionBeforeUpdate(permission models.PermissionType, updatePermissions []*models.DashboardAcl) (bool, error) {
return g.checkPermissionBeforeUpdate, nil
}
func (g *mockDashboardGuarder) GetAcl() ([]*models.DashboardAclInfoDTO, error) {
return nil, nil
}
type scenarioContext struct {
dashboardGuardianMock *mockDashboardGuarder
}
type scenarioFunc func(c *scenarioContext)
func dashboardGuardianScenario(desc string, mock *mockDashboardGuarder, fn scenarioFunc) {
Convey(desc, func() {
origNewDashboardGuardian := guardian.New
mockDashboardGuardian(mock)
sc := &scenarioContext{
dashboardGuardianMock: mock,
}
defer func() {
guardian.New = origNewDashboardGuardian
}()
fn(sc)
})
}
type dashboardPermissionScenarioContext struct {
dashboardGuardianMock *mockDashboardGuarder
}
type dashboardPermissionScenarioFunc func(sc *dashboardPermissionScenarioContext)
func dashboardPermissionScenario(desc string, mock *mockDashboardGuarder, fn dashboardPermissionScenarioFunc) {
Convey(desc, func() {
origNewDashboardGuardian := guardian.New
mockDashboardGuardian(mock)
sc := &dashboardPermissionScenarioContext{
dashboardGuardianMock: mock,
}
defer func() {
guardian.New = origNewDashboardGuardian
}()
fn(sc)
})
}
func permissionScenario(desc string, canSave bool, fn dashboardPermissionScenarioFunc) {
mock := &mockDashboardGuarder{
canSave: canSave,
}
dashboardPermissionScenario(desc, mock, fn)
}
func callSaveWithResult(cmd models.SaveDashboardCommand) *models.Dashboard {
dto := toSaveDashboardDto(cmd)
res, _ := dashboards.NewService().SaveDashboard(&dto)
return res
}
func callSaveWithError(cmd models.SaveDashboardCommand) error {
dto := toSaveDashboardDto(cmd)
_, err := dashboards.NewService().SaveDashboard(&dto)
return err
}
func dashboardServiceScenario(desc string, mock *mockDashboardGuarder, fn scenarioFunc) {
Convey(desc, func() {
origNewDashboardGuardian := guardian.New
mockDashboardGuardian(mock)
sc := &scenarioContext{
dashboardGuardianMock: mock,
}
defer func() {
guardian.New = origNewDashboardGuardian
}()
fn(sc)
})
}
func saveTestDashboard(title string, orgId int64, folderId int64) *models.Dashboard {
cmd := models.SaveDashboardCommand{
OrgId: orgId,
FolderId: folderId,
IsFolder: false,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": title,
}),
}
dto := dashboards.SaveDashboardDTO{
OrgId: orgId,
Dashboard: cmd.GetDashboardModel(),
User: &models.SignedInUser{
UserId: 1,
OrgRole: models.ROLE_ADMIN,
},
}
res, err := dashboards.NewService().SaveDashboard(&dto)
So(err, ShouldBeNil)
return res
}
func saveTestFolder(title string, orgId int64) *models.Dashboard {
cmd := models.SaveDashboardCommand{
OrgId: orgId,
FolderId: 0,
IsFolder: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": title,
}),
}
dto := dashboards.SaveDashboardDTO{
OrgId: orgId,
Dashboard: cmd.GetDashboardModel(),
User: &models.SignedInUser{
UserId: 1,
OrgRole: models.ROLE_ADMIN,
},
}
res, err := dashboards.NewService().SaveDashboard(&dto)
So(err, ShouldBeNil)
return res
}
func toSaveDashboardDto(cmd models.SaveDashboardCommand) dashboards.SaveDashboardDTO {
dash := (&cmd).GetDashboardModel()
return dashboards.SaveDashboardDTO{
Dashboard: dash,
Message: cmd.Message,
OrgId: cmd.OrgId,
User: &models.SignedInUser{UserId: cmd.UserId},
Overwrite: cmd.Overwrite,
}
}

View File

@@ -100,324 +100,6 @@ func TestDashboardDataAccess(t *testing.T) {
So(err, ShouldBeNil)
})
Convey("Should return not found error if no dashboard is found for update", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Overwrite: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": float64(123412321),
"title": "Expect error",
"tags": []interface{}{},
}),
}
err := SaveDashboard(&cmd)
So(err, ShouldEqual, m.ErrDashboardNotFound)
})
Convey("Should not be able to overwrite dashboard in another org", func() {
query := m.GetDashboardQuery{Slug: "test-dash-23", OrgId: 1}
GetDashboard(&query)
cmd := m.SaveDashboardCommand{
OrgId: 2,
Overwrite: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": float64(query.Result.Id),
"title": "Expect error",
"tags": []interface{}{},
}),
}
err := SaveDashboard(&cmd)
So(err, ShouldEqual, m.ErrDashboardNotFound)
})
Convey("Should be able to save dashboards with same name in different folders", func() {
firstSaveCmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": "test dash folder and title",
"tags": []interface{}{},
"uid": "randomHash",
}),
FolderId: 3,
}
err := SaveDashboard(&firstSaveCmd)
So(err, ShouldBeNil)
secondSaveCmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": "test dash folder and title",
"tags": []interface{}{},
"uid": "moreRandomHash",
}),
FolderId: 1,
}
err = SaveDashboard(&secondSaveCmd)
So(err, ShouldBeNil)
So(firstSaveCmd.Result.Id, ShouldNotEqual, secondSaveCmd.Result.Id)
})
Convey("Should be able to overwrite dashboard in same folder using title", func() {
insertTestDashboard("Dash", 1, 0, false, "prod", "webapp")
folder := insertTestDashboard("Folder", 1, 0, true, "prod", "webapp")
dashInFolder := insertTestDashboard("Dash", 1, folder.Id, false, "prod", "webapp")
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Dash",
}),
FolderId: folder.Id,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
So(cmd.Result.Id, ShouldEqual, dashInFolder.Id)
So(cmd.Result.Uid, ShouldEqual, dashInFolder.Uid)
})
Convey("Should be able to overwrite dashboard in General folder using title", func() {
dashInGeneral := insertTestDashboard("Dash", 1, 0, false, "prod", "webapp")
folder := insertTestDashboard("Folder", 1, 0, true, "prod", "webapp")
insertTestDashboard("Dash", 1, folder.Id, false, "prod", "webapp")
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Dash",
}),
FolderId: 0,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
So(cmd.Result.Id, ShouldEqual, dashInGeneral.Id)
So(cmd.Result.Uid, ShouldEqual, dashInGeneral.Uid)
})
Convey("Should not be able to overwrite folder with dashboard in general folder using title", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": savedFolder.Title,
}),
FolderId: 0,
IsFolder: false,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldEqual, m.ErrDashboardWithSameNameAsFolder)
})
Convey("Should not be able to overwrite folder with dashboard in folder using title", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": savedFolder.Title,
}),
FolderId: savedFolder.Id,
IsFolder: false,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldEqual, m.ErrDashboardWithSameNameAsFolder)
})
Convey("Should not be able to overwrite folder with dashboard using id", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedFolder.Id,
"title": "new title",
}),
IsFolder: false,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldEqual, m.ErrDashboardTypeMismatch)
})
Convey("Should not be able to overwrite dashboard with folder using id", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDash.Id,
"title": "new folder title",
}),
IsFolder: true,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldEqual, m.ErrDashboardTypeMismatch)
})
Convey("Should not be able to overwrite folder with dashboard using uid", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedFolder.Uid,
"title": "new title",
}),
IsFolder: false,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldEqual, m.ErrDashboardTypeMismatch)
})
Convey("Should not be able to overwrite dashboard with folder using uid", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDash.Uid,
"title": "new folder title",
}),
IsFolder: true,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldEqual, m.ErrDashboardTypeMismatch)
})
Convey("Should not be able to save dashboard with same name in the same folder without overwrite", func() {
firstSaveCmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": "test dash folder and title",
"tags": []interface{}{},
"uid": "randomHash",
}),
FolderId: 3,
}
err := SaveDashboard(&firstSaveCmd)
So(err, ShouldBeNil)
secondSaveCmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": "test dash folder and title",
"tags": []interface{}{},
"uid": "moreRandomHash",
}),
FolderId: 3,
}
err = SaveDashboard(&secondSaveCmd)
So(err, ShouldEqual, m.ErrDashboardWithSameNameInFolderExists)
})
Convey("Should be able to save and update dashboard using same uid", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"uid": "dsfalkjngailuedt",
"title": "test dash 23",
}),
}
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
err = SaveDashboard(&cmd)
So(err, ShouldBeNil)
})
Convey("Should be able to update dashboard using uid", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDash.Uid,
"title": "new title",
}),
FolderId: 0,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
Convey("Should be able to get updated dashboard by uid", func() {
query := m.GetDashboardQuery{
Uid: savedDash.Uid,
OrgId: 1,
}
err := GetDashboard(&query)
So(err, ShouldBeNil)
So(query.Result.Id, ShouldEqual, savedDash.Id)
So(query.Result.Title, ShouldEqual, "new title")
So(query.Result.FolderId, ShouldEqual, 0)
})
})
Convey("Should be able to update dashboard with the same title and folder id", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": "randomHash",
"title": "folderId",
"style": "light",
"tags": []interface{}{},
}),
FolderId: 2,
}
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
So(cmd.Result.FolderId, ShouldEqual, 2)
cmd = m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": cmd.Result.Id,
"uid": "randomHash",
"title": "folderId",
"style": "dark",
"version": cmd.Result.Version,
"tags": []interface{}{},
}),
FolderId: 2,
}
err = SaveDashboard(&cmd)
So(err, ShouldBeNil)
})
Convey("Should be able to update using uid without id and overwrite", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDash.Uid,
"title": "folderId",
"version": savedDash.Version,
"tags": []interface{}{},
}),
FolderId: savedDash.FolderId,
}
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
})
Convey("Should retry generation of uid once if it fails.", func() {
timesCalled := 0
generateNewUid = func() string {
@@ -499,6 +181,36 @@ func TestDashboardDataAccess(t *testing.T) {
So(len(query.Result), ShouldEqual, 0)
})
Convey("Should return error if no dashboard is found for update when dashboard id is greater than zero", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Overwrite: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": float64(123412321),
"title": "Expect error",
"tags": []interface{}{},
}),
}
err := SaveDashboard(&cmd)
So(err, ShouldEqual, m.ErrDashboardNotFound)
})
Convey("Should not return error if no dashboard is found for update when dashboard id is zero", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Overwrite: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": 0,
"title": "New dash",
"tags": []interface{}{},
}),
}
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
})
Convey("Should be able to get dashboard tags", func() {
query := m.GetDashboardTagsQuery{OrgId: 1}
@@ -627,6 +339,9 @@ func insertTestDashboard(title string, orgId int64, folderId int64, isFolder boo
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
cmd.Result.Data.Set("id", cmd.Result.Id)
cmd.Result.Data.Set("uid", cmd.Result.Uid)
return cmd.Result
}

View File

@@ -12,7 +12,7 @@ import (
)
func updateTestDashboard(dashboard *m.Dashboard, data map[string]interface{}) {
data["uid"] = dashboard.Uid
data["id"] = dashboard.Id
saveCmd := m.SaveDashboardCommand{
OrgId: dashboard.OrgId,

View File

@@ -1,61 +1,13 @@
package sqlstore
import (
"os"
"strings"
"testing"
"github.com/go-xorm/xorm"
. "github.com/smartystreets/goconvey/convey"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
)
var (
dbSqlite = "sqlite"
dbMySql = "mysql"
dbPostgres = "postgres"
)
func InitTestDB(t *testing.T) *xorm.Engine {
selectedDb := dbSqlite
//selectedDb := dbMySql
//selectedDb := dbPostgres
var x *xorm.Engine
var err error
// environment variable present for test db?
if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
selectedDb = db
}
switch strings.ToLower(selectedDb) {
case dbMySql:
x, err = xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr)
case dbPostgres:
x, err = xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr)
default:
x, err = xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr)
}
// x.ShowSQL()
if err != nil {
t.Fatalf("Failed to init in memory sqllite3 db %v", err)
}
sqlutil.CleanDB(x)
if err := SetEngine(x); err != nil {
t.Fatal(err)
}
return x
}
type Test struct {
Id int64
Name string

View File

@@ -7,14 +7,15 @@ import (
"path"
"path/filepath"
"strings"
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
"github.com/grafana/grafana/pkg/setting"
"github.com/go-sql-driver/mysql"
@@ -101,7 +102,6 @@ func SetEngine(engine *xorm.Engine) (err error) {
// Init repo instances
annotations.SetRepository(&SqlAnnotationRepo{})
dashboards.SetRepository(&dashboards.DashboardRepository{})
return nil
}
@@ -216,3 +216,46 @@ func LoadConfig() {
DbCfg.ServerCertName = sec.Key("server_cert_name").String()
DbCfg.Path = sec.Key("path").MustString("data/grafana.db")
}
var (
dbSqlite = "sqlite"
dbMySql = "mysql"
dbPostgres = "postgres"
)
func InitTestDB(t *testing.T) *xorm.Engine {
selectedDb := dbSqlite
//selectedDb := dbMySql
//selectedDb := dbPostgres
var x *xorm.Engine
var err error
// environment variable present for test db?
if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
selectedDb = db
}
switch strings.ToLower(selectedDb) {
case dbMySql:
x, err = xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr)
case dbPostgres:
x, err = xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr)
default:
x, err = xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr)
}
// x.ShowSQL()
if err != nil {
t.Fatalf("Failed to init in memory sqllite3 db %v", err)
}
sqlutil.CleanDB(x)
if err := SetEngine(x); err != nil {
t.Fatal(err)
}
return x
}

View File

@@ -1,7 +1,6 @@
package util
import (
"errors"
"regexp"
"github.com/teris-io/shortid"
@@ -11,25 +10,18 @@ var allowedChars = shortid.DefaultABC
var validUidPattern = regexp.MustCompile(`^[a-zA-Z0-9\-\_]*$`).MatchString
var ErrDashboardInvalidUid = errors.New("uid contains illegal characters")
var ErrDashboardUidToLong = errors.New("uid to long. max 40 characters")
func init() {
gen, _ := shortid.New(1, allowedChars, 1)
shortid.SetDefault(gen)
}
// VerifyUid verifies the size and content of the uid
func VerifyUid(uid string) error {
if len(uid) > 40 {
return ErrDashboardUidToLong
}
// IsValidShortUid checks if short unique identifier contains valid characters
func IsValidShortUid(uid string) bool {
if !validUidPattern(uid) {
return ErrDashboardInvalidUid
return false
}
return nil
return true
}
// GenerateShortUid generates a short unique identifier.

View File

@@ -4,8 +4,7 @@ import "testing"
func TestAllowedCharMatchesUidPattern(t *testing.T) {
for _, c := range allowedChars {
err := VerifyUid(string(c))
if err != nil {
if !IsValidShortUid(string(c)) {
t.Fatalf("charset for creating new shortids contains chars not present in uid pattern")
}
}