From 53cd39fde560d206057ce97357ad82eb90f5eaab Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 19 Feb 2018 11:12:56 +0100 Subject: [PATCH] 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 --- pkg/api/alerting.go | 2 +- pkg/api/annotations.go | 2 +- pkg/api/dashboard.go | 61 +- pkg/api/dashboard_acl.go | 4 +- pkg/api/dashboard_test.go | 234 ++--- pkg/api/plugins.go | 2 +- pkg/models/alert.go | 14 + pkg/models/dashboards.go | 62 +- pkg/plugins/dashboard_importer.go | 24 +- pkg/plugins/dashboard_importer_test.go | 25 +- pkg/plugins/dashboards_updater.go | 2 +- pkg/services/alerting/commands.go | 16 +- pkg/services/dashboards/dashboard_service.go | 231 ++++ .../dashboards/dashboard_service_test.go | 144 +++ pkg/services/dashboards/dashboards.go | 143 --- pkg/services/dashboards/dashboards_test.go | 43 - pkg/services/guardian/guardian.go | 36 +- .../provisioning/dashboards/file_reader.go | 32 +- .../dashboards/file_reader_test.go | 52 +- pkg/services/sqlstore/dashboard.go | 253 +++-- .../dashboard_service_integration_test.go | 984 ++++++++++++++++++ pkg/services/sqlstore/dashboard_test.go | 351 +------ .../sqlstore/dashboard_version_test.go | 2 +- pkg/services/sqlstore/datasource_test.go | 48 - pkg/services/sqlstore/sqlstore.go | 47 +- pkg/util/shortid_generator.go | 16 +- pkg/util/shortid_generator_test.go | 3 +- 27 files changed, 1896 insertions(+), 937 deletions(-) create mode 100644 pkg/services/dashboards/dashboard_service.go create mode 100644 pkg/services/dashboards/dashboard_service_test.go delete mode 100644 pkg/services/dashboards/dashboards.go delete mode 100644 pkg/services/dashboards/dashboards_test.go create mode 100644 pkg/services/sqlstore/dashboard_service_integration_test.go diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index a8bad638d43..08edf54748b 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -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) diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index e3845520795..cb1a15e69eb 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -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 } diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 32a09bcd931..a079f6f7681 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -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) } diff --git a/pkg/api/dashboard_acl.go b/pkg/api/dashboard_acl.go index 32b75e80cc0..13b29db78e6 100644 --- a/pkg/api/dashboard_acl.go +++ b/pkg/api/dashboard_acl.go @@ -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) } diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 4a45c561d57..046075fc622 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -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) }) } diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 0483b624a30..e8c21541339 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -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, diff --git a/pkg/models/alert.go b/pkg/models/alert.go index 6039ecef6ba..88b49350b97 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -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 +} diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index f5c06fc3432..5bf37136548 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -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 // diff --git a/pkg/plugins/dashboard_importer.go b/pkg/plugins/dashboard_importer.go index 9036b943b30..558f61871ba 100644 --- a/pkg/plugins/dashboard_importer.go +++ b/pkg/plugins/dashboard_importer.go @@ -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, } diff --git a/pkg/plugins/dashboard_importer_test.go b/pkg/plugins/dashboard_importer_test.go index a13dc8fe0a5..549b3bb4cf9 100644 --- a/pkg/plugins/dashboard_importer_test.go +++ b/pkg/plugins/dashboard_importer_test.go @@ -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()) { diff --git a/pkg/plugins/dashboards_updater.go b/pkg/plugins/dashboards_updater.go index 4c40e536d14..835e8873810 100644 --- a/pkg/plugins/dashboards_updater.go +++ b/pkg/plugins/dashboards_updater.go @@ -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, } diff --git a/pkg/services/alerting/commands.go b/pkg/services/alerting/commands.go index 62671a559fa..2c145614751 100644 --- a/pkg/services/alerting/commands.go +++ b/pkg/services/alerting/commands.go @@ -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, diff --git a/pkg/services/dashboards/dashboard_service.go b/pkg/services/dashboards/dashboard_service.go new file mode 100644 index 00000000000..ffae62860a6 --- /dev/null +++ b/pkg/services/dashboards/dashboard_service.go @@ -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 + } +} diff --git a/pkg/services/dashboards/dashboard_service_test.go b/pkg/services/dashboards/dashboard_service_test.go new file mode 100644 index 00000000000..4a7dba762f6 --- /dev/null +++ b/pkg/services/dashboards/dashboard_service_test.go @@ -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 +} diff --git a/pkg/services/dashboards/dashboards.go b/pkg/services/dashboards/dashboards.go deleted file mode 100644 index 9bf4eb6faec..00000000000 --- a/pkg/services/dashboards/dashboards.go +++ /dev/null @@ -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 -} diff --git a/pkg/services/dashboards/dashboards_test.go b/pkg/services/dashboards/dashboards_test.go deleted file mode 100644 index 3cf6cd1f489..00000000000 --- a/pkg/services/dashboards/dashboards_test.go +++ /dev/null @@ -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) - } - } -} diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index 05795b7f2df..98db5449182 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -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 } diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index f1e8148e70d..d3e9892c8f5 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -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 } diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index a81b502c50a..cd5e3456734 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -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 diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 42c83da8810..8516e7c46e7 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -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 + }) +} diff --git a/pkg/services/sqlstore/dashboard_service_integration_test.go b/pkg/services/sqlstore/dashboard_service_integration_test.go new file mode 100644 index 00000000000..e4bf93c2f58 --- /dev/null +++ b/pkg/services/sqlstore/dashboard_service_integration_test.go @@ -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, + } +} diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index 7de4c5f5701..51a2d4eb64e 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -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 } diff --git a/pkg/services/sqlstore/dashboard_version_test.go b/pkg/services/sqlstore/dashboard_version_test.go index e20ac897b3d..1b74e7847c4 100644 --- a/pkg/services/sqlstore/dashboard_version_test.go +++ b/pkg/services/sqlstore/dashboard_version_test.go @@ -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, diff --git a/pkg/services/sqlstore/datasource_test.go b/pkg/services/sqlstore/datasource_test.go index 28f5b8eba9d..90300e20029 100644 --- a/pkg/services/sqlstore/datasource_test.go +++ b/pkg/services/sqlstore/datasource_test.go @@ -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 diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 2655bf9c22e..5843c5c300b 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -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 +} diff --git a/pkg/util/shortid_generator.go b/pkg/util/shortid_generator.go index f2d9faa61c8..d87b6f70fe6 100644 --- a/pkg/util/shortid_generator.go +++ b/pkg/util/shortid_generator.go @@ -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. diff --git a/pkg/util/shortid_generator_test.go b/pkg/util/shortid_generator_test.go index 548163267dc..359e054a0ca 100644 --- a/pkg/util/shortid_generator_test.go +++ b/pkg/util/shortid_generator_test.go @@ -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") } }