diff --git a/pkg/api/acl.go b/pkg/api/acl.go new file mode 100644 index 00000000000..6c839a2f11a --- /dev/null +++ b/pkg/api/acl.go @@ -0,0 +1,10 @@ +package api + +import "github.com/grafana/grafana/pkg/models" + +// updateDashboardACL updates a dashboard's ACL items. +// +// Stubbable by tests. +var updateDashboardACL = func(hs *HTTPServer, dashID int64, items []*models.DashboardAcl) error { + return hs.SQLStore.UpdateDashboardACL(dashID, items) +} diff --git a/pkg/api/api.go b/pkg/api/api.go index c2803af4cb3..1f1b493f83f 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -266,7 +266,7 @@ func (hs *HTTPServer) registerRoutes() { apiRoute.Get("/datasources/id/:name", routing.Wrap(GetDataSourceIdByName), reqSignedIn) apiRoute.Get("/plugins", routing.Wrap(hs.GetPluginList)) - apiRoute.Get("/plugins/:pluginId/settings", routing.Wrap(GetPluginSettingByID)) + apiRoute.Get("/plugins/:pluginId/settings", routing.Wrap(hs.GetPluginSettingByID)) apiRoute.Get("/plugins/:pluginId/markdown/:name", routing.Wrap(hs.GetPluginMarkdown)) apiRoute.Get("/plugins/:pluginId/health", routing.Wrap(hs.CheckHealth)) apiRoute.Any("/plugins/:pluginId/resources", hs.CallResource) @@ -288,13 +288,13 @@ func (hs *HTTPServer) registerRoutes() { // Folders apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) { - folderRoute.Get("/", routing.Wrap(GetFolders)) - folderRoute.Get("/id/:id", routing.Wrap(GetFolderByID)) + folderRoute.Get("/", routing.Wrap(hs.GetFolders)) + folderRoute.Get("/id/:id", routing.Wrap(hs.GetFolderByID)) folderRoute.Post("/", bind(models.CreateFolderCommand{}), routing.Wrap(hs.CreateFolder)) folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) { - folderUidRoute.Get("/", routing.Wrap(GetFolderByUID)) - folderUidRoute.Put("/", bind(models.UpdateFolderCommand{}), routing.Wrap(UpdateFolder)) + folderUidRoute.Get("/", routing.Wrap(hs.GetFolderByUID)) + folderUidRoute.Put("/", bind(models.UpdateFolderCommand{}), routing.Wrap(hs.UpdateFolder)) folderUidRoute.Delete("/", routing.Wrap(hs.DeleteFolder)) folderUidRoute.Group("/permissions", func(folderPermissionRoute routing.RouteRegister) { diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 7789ab3f128..c7563b1a821 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -8,7 +8,6 @@ import ( "path/filepath" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/dashboards" @@ -122,7 +121,7 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { meta.FolderUrl = query.Result.GetUrl() } - svc := dashboards.NewProvisioningService() + svc := dashboards.NewProvisioningService(hs.SQLStore) provisioningData, err := svc.GetProvisionedDashboardDataByDashboardID(dash.Id) if err != nil { return response.Error(500, "Error while checking if dashboard is provisioned", err) @@ -227,7 +226,7 @@ func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response { } } - svc := dashboards.NewService() + svc := dashboards.NewService(hs.SQLStore) err := svc.DeleteDashboard(dash.Id, c.OrgId) if err != nil { var dashboardErr models.DashboardErr @@ -264,7 +263,7 @@ func (hs *HTTPServer) PostDashboard(c *models.ReqContext, cmd models.SaveDashboa } } - svc := dashboards.NewProvisioningService() + svc := dashboards.NewProvisioningService(hs.SQLStore) provisioningData, err := svc.GetProvisionedDashboardDataByDashboardID(dash.Id) if err != nil { return response.Error(500, "Error while checking if dashboard is provisioned", err) @@ -291,15 +290,15 @@ func (hs *HTTPServer) PostDashboard(c *models.ReqContext, cmd models.SaveDashboa Overwrite: cmd.Overwrite, } - dashSvc := dashboards.NewService() + dashSvc := dashboards.NewService(hs.SQLStore) dashboard, err := dashSvc.SaveDashboard(dashItem, allowUiUpdate) if err != nil { - return dashboardSaveErrorToApiResponse(err) + return hs.dashboardSaveErrorToApiResponse(err) } if hs.Cfg.EditorsCanAdmin && newDashboard { inFolder := cmd.FolderId > 0 - err := dashboards.MakeUserAdmin(hs.Bus, cmd.OrgId, cmd.UserId, dashboard.Id, !inFolder) + err := dashSvc.MakeUserAdmin(cmd.OrgId, cmd.UserId, dashboard.Id, !inFolder) if err != nil { hs.log.Error("Could not make user admin", "dashboard", dashboard.Title, "user", c.SignedInUser.UserId, "error", err) } @@ -335,7 +334,7 @@ func (hs *HTTPServer) PostDashboard(c *models.ReqContext, cmd models.SaveDashboa }) } -func dashboardSaveErrorToApiResponse(err error) response.Response { +func (hs *HTTPServer) dashboardSaveErrorToApiResponse(err error) response.Response { var dashboardErr models.DashboardErr if ok := errors.As(err, &dashboardErr); ok { if body := dashboardErr.Body(); body != nil { @@ -360,7 +359,7 @@ func dashboardSaveErrorToApiResponse(err error) response.Response { if ok := errors.As(err, &pluginErr); ok { message := fmt.Sprintf("The dashboard belongs to plugin %s.", pluginErr.PluginId) // look up plugin name - if pluginDef, exist := manager.Plugins[pluginErr.PluginId]; exist { + if pluginDef := hs.PluginManager.GetPlugin(pluginErr.PluginId); pluginDef != nil { message = fmt.Sprintf("The dashboard belongs to plugin %s.", pluginDef.Name) } return response.JSON(412, util.DynMap{"status": "plugin-dashboard", "message": message}) diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go index df9dae8e705..f609bd65857 100644 --- a/pkg/api/dashboard_permission.go +++ b/pkg/api/dashboard_permission.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/guardian" ) @@ -68,11 +67,9 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext, apiCmd dt return dashboardGuardianResponse(err) } - cmd := models.UpdateDashboardAclCommand{} - cmd.DashboardID = dashID - + var items []*models.DashboardAcl for _, item := range apiCmd.Items { - cmd.Items = append(cmd.Items, &models.DashboardAcl{ + items = append(items, &models.DashboardAcl{ OrgID: c.OrgId, DashboardID: dashID, UserID: item.UserID, @@ -88,9 +85,9 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext, apiCmd dt if err != nil { return response.Error(500, "Error while retrieving hidden permissions", err) } - cmd.Items = append(cmd.Items, hiddenACL...) + items = append(items, hiddenACL...) - if okToUpdate, err := g.CheckPermissionBeforeUpdate(models.PERMISSION_ADMIN, cmd.Items); err != nil || !okToUpdate { + if okToUpdate, err := g.CheckPermissionBeforeUpdate(models.PERMISSION_ADMIN, items); err != nil || !okToUpdate { if err != nil { if errors.Is(err, guardian.ErrGuardianPermissionExists) || errors.Is(err, guardian.ErrGuardianOverride) { return response.Error(400, err.Error(), err) @@ -102,7 +99,7 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext, apiCmd dt return response.Error(403, "Cannot remove own admin permission for a folder", nil) } - if err := bus.Dispatch(&cmd); err != nil { + if err := updateDashboardACL(hs, dashID, items); err != nil { if errors.Is(err, models.ErrDashboardAclInfoMissing) || errors.Is(err, models.ErrDashboardPermissionDashboardEmpty) { return response.Error(409, err.Error(), err) diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index 20a9fe8e45b..8e0adf8848d 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -48,7 +48,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { cmd: cmd, fn: func(sc *scenarioContext) { setUp() - callUpdateDashboardPermissions(sc) + callUpdateDashboardPermissions(t, sc) assert.Equal(t, 404, sc.resp.Code) }, }, hs) @@ -91,7 +91,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { cmd: cmd, fn: func(sc *scenarioContext) { setUp() - callUpdateDashboardPermissions(sc) + callUpdateDashboardPermissions(t, sc) assert.Equal(t, 403, sc.resp.Code) }, }, hs) @@ -151,7 +151,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { cmd: cmd, fn: func(sc *scenarioContext) { setUp() - callUpdateDashboardPermissions(sc) + callUpdateDashboardPermissions(t, sc) assert.Equal(t, 200, sc.resp.Code) }, }, hs) @@ -189,7 +189,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { cmd: cmd, fn: func(sc *scenarioContext) { setUp() - callUpdateDashboardPermissions(sc) + callUpdateDashboardPermissions(t, sc) assert.Equal(t, 400, sc.resp.Code) }, }, hs) @@ -217,7 +217,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { routePattern: "/api/dashboards/id/:id/permissions", cmd: cmd, fn: func(sc *scenarioContext) { - callUpdateDashboardPermissions(sc) + callUpdateDashboardPermissions(t, sc) assert.Equal(t, 400, sc.resp.Code) respJSON, err := jsonMap(sc.resp.Body.Bytes()) require.NoError(t, err) @@ -260,7 +260,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { cmd: cmd, fn: func(sc *scenarioContext) { setUp() - callUpdateDashboardPermissions(sc) + callUpdateDashboardPermissions(t, sc) assert.Equal(t, 400, sc.resp.Code) }, }, hs) @@ -335,13 +335,20 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { cmd: cmd, fn: func(sc *scenarioContext) { setUp() - bus.AddHandler("test", func(cmd *models.UpdateDashboardAclCommand) error { - assert.Len(t, cmd.Items, 4) - return nil + // TODO: Replace this fake with a fake SQLStore instead (once we can use an interface in its stead) + origUpdateDashboardACL := updateDashboardACL + t.Cleanup(func() { + updateDashboardACL = origUpdateDashboardACL }) + var gotItems []*models.DashboardAcl + updateDashboardACL = func(hs *HTTPServer, folderID int64, items []*models.DashboardAcl) error { + gotItems = items + return nil + } sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) + assert.Len(t, gotItems, 4) }, }, hs) }) @@ -353,10 +360,16 @@ func callGetDashboardPermissions(sc *scenarioContext, hs *HTTPServer) { sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() } -func callUpdateDashboardPermissions(sc *scenarioContext) { - bus.AddHandler("test", func(cmd *models.UpdateDashboardAclCommand) error { - return nil +func callUpdateDashboardPermissions(t *testing.T, sc *scenarioContext) { + t.Helper() + + origUpdateDashboardACL := updateDashboardACL + t.Cleanup(func() { + updateDashboardACL = origUpdateDashboardACL }) + updateDashboardACL = func(hs *HTTPServer, dashID int64, items []*models.DashboardAcl) error { + return nil + } sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() } diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 2355871e594..ae4d1a759fc 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" + dboards "github.com/grafana/grafana/pkg/dashboards" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/dashboards" @@ -28,7 +29,10 @@ func TestGetHomeDashboard(t *testing.T) { cfg := setting.NewCfg() cfg.StaticRootPath = "../../public/" - hs := &HTTPServer{Cfg: cfg, Bus: bus.New()} + hs := &HTTPServer{ + Cfg: cfg, Bus: bus.New(), + PluginManager: &fakePluginManager{}, + } hs.Bus.AddHandler(func(query *models.GetPreferencesWithDefaultsQuery) error { query.Result = &models.Preferences{ HomeDashboardId: 0, @@ -102,11 +106,6 @@ func TestDashboardAPIEndpoint(t *testing.T) { return nil }) - bus.AddHandler("test", func(query *models.GetProvisionedDashboardDataByIdQuery) error { - query.Result = nil - return nil - }) - viewerRole := models.ROLE_VIEWER editorRole := models.ROLE_EDITOR @@ -165,7 +164,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { state := setUp() - CallDeleteDashboardBySlug(sc, &HTTPServer{Cfg: setting.NewCfg()}) + callDeleteDashboardBySlug(sc, &HTTPServer{Cfg: setting.NewCfg()}) assert.Equal(t, 403, sc.resp.Code) assert.Equal(t, "child-dash", state.dashQueries[0].Slug) @@ -175,7 +174,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { state := setUp() - CallDeleteDashboardByUID(sc, &HTTPServer{Cfg: setting.NewCfg()}) + callDeleteDashboardByUID(sc, &HTTPServer{Cfg: setting.NewCfg()}) assert.Equal(t, 403, sc.resp.Code) assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) @@ -230,7 +229,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { state := setUp() - CallDeleteDashboardBySlug(sc, &HTTPServer{Cfg: setting.NewCfg()}) + callDeleteDashboardBySlug(sc, &HTTPServer{Cfg: setting.NewCfg()}) assert.Equal(t, 200, sc.resp.Code) assert.Equal(t, "child-dash", state.dashQueries[0].Slug) }) @@ -239,7 +238,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { state := setUp() - CallDeleteDashboardByUID(sc, &HTTPServer{Cfg: setting.NewCfg()}) + callDeleteDashboardByUID(sc, &HTTPServer{Cfg: setting.NewCfg()}) assert.Equal(t, 200, sc.resp.Code) assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) }) @@ -281,11 +280,6 @@ func TestDashboardAPIEndpoint(t *testing.T) { }) setting.ViewersCanEdit = false - bus.AddHandler("test", func(query *models.GetProvisionedDashboardDataByIdQuery) error { - query.Result = nil - return nil - }) - bus.AddHandler("test", func(query *models.GetDashboardsBySlugQuery) error { dashboards := []*models.Dashboard{fakeDash} query.Result = dashboards @@ -354,7 +348,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { state := setUp() - CallDeleteDashboardBySlug(sc, hs) + callDeleteDashboardBySlug(sc, hs) assert.Equal(t, 403, sc.resp.Code) assert.Equal(t, "child-dash", state.dashQueries[0].Slug) }) @@ -363,7 +357,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { state := setUp() - CallDeleteDashboardByUID(sc, hs) + callDeleteDashboardByUID(sc, hs) assert.Equal(t, 403, sc.resp.Code) assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) }) @@ -414,7 +408,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { state := setUp() - CallDeleteDashboardBySlug(sc, hs) + callDeleteDashboardBySlug(sc, hs) assert.Equal(t, 403, sc.resp.Code) assert.Equal(t, "child-dash", state.dashQueries[0].Slug) }) @@ -423,7 +417,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { state := setUp() - CallDeleteDashboardByUID(sc, hs) + callDeleteDashboardByUID(sc, hs) assert.Equal(t, 403, sc.resp.Code) assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) }) @@ -492,7 +486,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { state := setUpInner() - CallDeleteDashboardBySlug(sc, hs) + callDeleteDashboardBySlug(sc, hs) assert.Equal(t, 200, sc.resp.Code) assert.Equal(t, "child-dash", state.dashQueries[0].Slug) }) @@ -500,7 +494,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { state := setUpInner() - CallDeleteDashboardByUID(sc, hs) + callDeleteDashboardByUID(sc, hs) assert.Equal(t, 200, sc.resp.Code) assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) }) @@ -570,7 +564,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { state := setUpInner() - CallDeleteDashboardBySlug(sc, hs) + callDeleteDashboardBySlug(sc, hs) assert.Equal(t, 403, sc.resp.Code) assert.Equal(t, "child-dash", state.dashQueries[0].Slug) }) @@ -578,7 +572,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { state := setUpInner() - CallDeleteDashboardByUID(sc, hs) + callDeleteDashboardByUID(sc, hs) assert.Equal(t, 403, sc.resp.Code) assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) }) @@ -624,7 +618,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { state := setUpInner() - CallDeleteDashboardBySlug(sc, hs) + callDeleteDashboardBySlug(sc, hs) assert.Equal(t, 200, sc.resp.Code) assert.Equal(t, "child-dash", state.dashQueries[0].Slug) }) @@ -632,7 +626,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { state := setUpInner() - CallDeleteDashboardByUID(sc, hs) + callDeleteDashboardByUID(sc, hs) assert.Equal(t, 200, sc.resp.Code) assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) }) @@ -690,7 +684,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { state := setUpInner() - CallDeleteDashboardBySlug(sc, hs) + callDeleteDashboardBySlug(sc, hs) assert.Equal(t, 403, sc.resp.Code) assert.Equal(t, "child-dash", state.dashQueries[0].Slug) }) @@ -698,7 +692,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { state := setUpInner() - CallDeleteDashboardByUID(sc, hs) + callDeleteDashboardByUID(sc, hs) assert.Equal(t, 403, sc.resp.Code) assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) }) @@ -730,11 +724,6 @@ func TestDashboardAPIEndpoint(t *testing.T) { dashTwo.FolderId = 3 dashTwo.HasAcl = false - bus.AddHandler("test", func(query *models.GetProvisionedDashboardDataByIdQuery) error { - query.Result = nil - return nil - }) - bus.AddHandler("test", func(query *models.GetDashboardsBySlugQuery) error { dashboards := []*models.Dashboard{dashOne, dashTwo} query.Result = dashboards @@ -743,14 +732,15 @@ func TestDashboardAPIEndpoint(t *testing.T) { role := models.ROLE_EDITOR - loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { - CallDeleteDashboardBySlug(sc, &HTTPServer{Cfg: setting.NewCfg()}) + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/dash", + "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { + callDeleteDashboardBySlug(sc, &HTTPServer{Cfg: setting.NewCfg()}) - assert.Equal(t, 412, sc.resp.Code) - result := sc.ToJSON() - assert.Equal(t, "multiple-slugs-exists", result.Get("status").MustString()) - assert.Equal(t, models.ErrDashboardsWithSameSlugExists.Error(), result.Get("message").MustString()) - }) + assert.Equal(t, 412, sc.resp.Code) + result := sc.ToJSON() + assert.Equal(t, "multiple-slugs-exists", result.Get("status").MustString()) + assert.Equal(t, models.ErrDashboardsWithSameSlugExists.Error(), result.Get("message").MustString()) + }) }) t.Run("Post dashboard response tests", func(t *testing.T) { @@ -856,11 +846,6 @@ func TestDashboardAPIEndpoint(t *testing.T) { return nil }) - bus.AddHandler("test", func(query *models.GetProvisionedDashboardDataByIdQuery) error { - query.Result = nil - return nil - }) - bus.AddHandler("test", func(query *models.GetDashboardVersionQuery) error { query.Result = &models.DashboardVersion{ Data: simplejson.NewFromAny(map[string]interface{}{ @@ -1017,10 +1002,13 @@ func TestDashboardAPIEndpoint(t *testing.T) { return nil }) - bus.AddHandler("test", func(query *models.GetProvisionedDashboardDataByIdQuery) error { - query.Result = &models.DashboardProvisioning{ExternalId: "/tmp/grafana/dashboards/test/dashboard1.json"} - return nil + origGetProvisionedData := dashboards.GetProvisionedData + t.Cleanup(func() { + dashboards.GetProvisionedData = origGetProvisionedData }) + dashboards.GetProvisionedData = func(dboards.Store, int64) (*models.DashboardProvisioning, error) { + return &models.DashboardProvisioning{ExternalId: "/tmp/grafana/dashboards/test/dashboard1.json"}, nil + } bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { query.Result = []*models.DashboardAclInfoDTO{ @@ -1030,20 +1018,21 @@ func TestDashboardAPIEndpoint(t *testing.T) { }) } - loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/dash", "/api/dashboards/db/:slug", models.ROLE_EDITOR, func(sc *scenarioContext) { - setUp() + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/dash", + "/api/dashboards/db/:slug", models.ROLE_EDITOR, func(sc *scenarioContext) { + setUp() - CallDeleteDashboardBySlug(sc, &HTTPServer{Cfg: setting.NewCfg()}) + callDeleteDashboardBySlug(sc, &HTTPServer{Cfg: setting.NewCfg()}) - assert.Equal(t, 400, sc.resp.Code) - result := sc.ToJSON() - assert.Equal(t, models.ErrDashboardCannotDeleteProvisionedDashboard.Error(), result.Get("error").MustString()) - }) + assert.Equal(t, 400, sc.resp.Code) + result := sc.ToJSON() + assert.Equal(t, models.ErrDashboardCannotDeleteProvisionedDashboard.Error(), result.Get("error").MustString()) + }) loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/abcdefghi", "/api/dashboards/db/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) { setUp() - CallDeleteDashboardByUID(sc, &HTTPServer{Cfg: setting.NewCfg()}) + callDeleteDashboardByUID(sc, &HTTPServer{Cfg: setting.NewCfg()}) assert.Equal(t, 400, sc.resp.Code) result := sc.ToJSON() @@ -1141,7 +1130,7 @@ func callGetDashboardVersions(sc *scenarioContext) { sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() } -func CallDeleteDashboardBySlug(sc *scenarioContext, hs *HTTPServer) { +func callDeleteDashboardBySlug(sc *scenarioContext, hs *HTTPServer) { bus.AddHandler("test", func(cmd *models.DeleteDashboardCommand) error { return nil }) @@ -1150,7 +1139,7 @@ func CallDeleteDashboardBySlug(sc *scenarioContext, hs *HTTPServer) { sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() } -func CallDeleteDashboardByUID(sc *scenarioContext, hs *HTTPServer) { +func callDeleteDashboardByUID(sc *scenarioContext, hs *HTTPServer) { bus.AddHandler("test", func(cmd *models.DeleteDashboardCommand) error { return nil }) @@ -1187,6 +1176,7 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s QuotaService: "a.QuotaService{ Cfg: cfg, }, + PluginManager: &fakePluginManager{}, } sc := setupScenarioContext(t, url) @@ -1204,7 +1194,7 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s dashboards.NewProvisioningService = origProvisioningService }) dashboards.MockDashboardService(mock) - dashboards.NewProvisioningService = func() dashboards.DashboardProvisioningService { + dashboards.NewProvisioningService = func(dboards.Store) dashboards.DashboardProvisioningService { return mockDashboardProvisioningService{} } @@ -1268,7 +1258,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout dashboards.NewService = origNewDashboardService dashboards.NewProvisioningService = origProvisioningService }) - dashboards.NewProvisioningService = func() dashboards.DashboardProvisioningService { + dashboards.NewProvisioningService = func(dboards.Store) dashboards.DashboardProvisioningService { return mockDashboardProvisioningService{} } dashboards.MockDashboardService(mock) @@ -1290,6 +1280,7 @@ type mockDashboardProvisioningService struct { dashboards.DashboardProvisioningService } -func (m mockDashboardProvisioningService) GetProvisionedDashboardDataByDashboardID(dashboardId int64) (*models.DashboardProvisioning, error) { - return &models.DashboardProvisioning{}, nil +func (s mockDashboardProvisioningService) GetProvisionedDashboardDataByDashboardID(dashboardID int64) ( + *models.DashboardProvisioning, error) { + return nil, nil } diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 4b9b90da781..2b024ffcd54 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -10,7 +10,6 @@ import ( "github.com/grafana/grafana/pkg/api/pluginproxy" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/plugins/manager" ) // ProxyDataSourceRequest proxies datasource requests @@ -35,8 +34,8 @@ func (hs *HTTPServer) ProxyDataSourceRequest(c *models.ReqContext) { } // find plugin - plugin, ok := manager.DataSources[ds.Type] - if !ok { + plugin := hs.PluginManager.GetDataSource(ds.Type) + if plugin == nil { c.JsonApiErr(http.StatusInternalServerError, "Unable to find datasource plugin", err) return } diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 7ba1d612e72..970c3b2ced0 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins/adapters" - "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/util" ) @@ -47,7 +46,7 @@ func (hs *HTTPServer) GetDataSources(c *models.ReqContext) response.Response { ReadOnly: ds.ReadOnly, } - if plugin, exists := manager.DataSources[ds.Type]; exists { + if plugin := hs.PluginManager.GetDataSource(ds.Type); plugin != nil { dsItem.TypeLogoUrl = plugin.Info.Logos.Small dsItem.TypeName = plugin.Name } else { @@ -363,8 +362,8 @@ func (hs *HTTPServer) CallDatasourceResource(c *models.ReqContext) { } // find plugin - plugin, ok := manager.DataSources[ds.Type] - if !ok { + plugin := hs.PluginManager.GetDataSource(ds.Type) + if plugin == nil { c.JsonApiErr(500, "Unable to find datasource plugin", err) return } @@ -428,8 +427,8 @@ func (hs *HTTPServer) CheckDatasourceHealth(c *models.ReqContext) response.Respo return response.Error(500, "Unable to load datasource metadata", err) } - plugin, ok := hs.PluginManager.GetDatasource(ds.Type) - if !ok { + plugin := hs.PluginManager.GetDataSource(ds.Type) + if plugin == nil { return response.Error(500, "Unable to find datasource plugin", err) } diff --git a/pkg/api/datasources_test.go b/pkg/api/datasources_test.go index 5aa66144f1b..3348857d488 100644 --- a/pkg/api/datasources_test.go +++ b/pkg/api/datasources_test.go @@ -35,7 +35,11 @@ func TestDataSourcesProxy_userLoggedIn(t *testing.T) { }) // handler func being tested - hs := &HTTPServer{Bus: bus.GetBus(), Cfg: setting.NewCfg()} + hs := &HTTPServer{ + Bus: bus.GetBus(), + Cfg: setting.NewCfg(), + PluginManager: &fakePluginManager{}, + } sc.handlerFunc = hs.GetDataSources sc.fakeReq("GET", "/api/datasources").exec() diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go index e6c7cc12bdc..4b83e443c1d 100644 --- a/pkg/api/dtos/plugins.go +++ b/pkg/api/dtos/plugins.go @@ -3,7 +3,6 @@ package dtos import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/manager" ) type PluginSetting struct { @@ -64,6 +63,6 @@ type ImportDashboardCommand struct { Path string `json:"path"` Overwrite bool `json:"overwrite"` Dashboard *simplejson.Json `json:"dashboard"` - Inputs []manager.ImportDashboardInput `json:"inputs"` + Inputs []plugins.ImportDashboardInput `json:"inputs"` FolderId int64 `json:"folderId"` } diff --git a/pkg/api/fakes.go b/pkg/api/fakes.go new file mode 100644 index 00000000000..a62be9409fe --- /dev/null +++ b/pkg/api/fakes.go @@ -0,0 +1,19 @@ +package api + +import "github.com/grafana/grafana/pkg/plugins" + +type fakePluginManager struct { + plugins.Manager +} + +func (pm *fakePluginManager) GetPlugin(id string) *plugins.PluginBase { + return nil +} + +func (pm *fakePluginManager) GetDataSource(id string) *plugins.DataSourcePlugin { + return nil +} + +func (pm *fakePluginManager) Renderer() *plugins.RendererPlugin { + return nil +} diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 644ba2ef3c8..5c3b3f5b283 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -14,8 +14,8 @@ import ( "github.com/grafana/grafana/pkg/util" ) -func GetFolders(c *models.ReqContext) response.Response { - s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) +func (hs *HTTPServer) GetFolders(c *models.ReqContext) response.Response { + s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) folders, err := s.GetFolders(c.QueryInt64("limit")) if err != nil { @@ -35,10 +35,9 @@ func GetFolders(c *models.ReqContext) response.Response { return response.JSON(200, result) } -func GetFolderByUID(c *models.ReqContext) response.Response { - s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) +func (hs *HTTPServer) GetFolderByUID(c *models.ReqContext) response.Response { + s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) folder, err := s.GetFolderByUID(c.Params(":uid")) - if err != nil { return toFolderError(err) } @@ -47,8 +46,8 @@ func GetFolderByUID(c *models.ReqContext) response.Response { return response.JSON(200, toFolderDto(g, folder)) } -func GetFolderByID(c *models.ReqContext) response.Response { - s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) +func (hs *HTTPServer) GetFolderByID(c *models.ReqContext) response.Response { + s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) folder, err := s.GetFolderByID(c.ParamsInt64(":id")) if err != nil { return toFolderError(err) @@ -59,24 +58,25 @@ func GetFolderByID(c *models.ReqContext) response.Response { } func (hs *HTTPServer) CreateFolder(c *models.ReqContext, cmd models.CreateFolderCommand) response.Response { - s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) - err := s.CreateFolder(&cmd) + s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) + folder, err := s.CreateFolder(cmd.Title, cmd.Uid) if err != nil { return toFolderError(err) } if hs.Cfg.EditorsCanAdmin { - if err := dashboards.MakeUserAdmin(hs.Bus, c.OrgId, c.SignedInUser.UserId, cmd.Result.Id, true); err != nil { - hs.log.Error("Could not make user admin", "folder", cmd.Result.Title, "user", c.SignedInUser.UserId, "error", err) + if err := s.MakeUserAdmin(c.OrgId, c.SignedInUser.UserId, folder.Id, true); err != nil { + hs.log.Error("Could not make user admin", "folder", folder.Title, "user", + c.SignedInUser.UserId, "error", err) } } - g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser) - return response.JSON(200, toFolderDto(g, cmd.Result)) + g := guardian.New(folder.Id, c.OrgId, c.SignedInUser) + return response.JSON(200, toFolderDto(g, folder)) } -func UpdateFolder(c *models.ReqContext, cmd models.UpdateFolderCommand) response.Response { - s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) +func (hs *HTTPServer) UpdateFolder(c *models.ReqContext, cmd models.UpdateFolderCommand) response.Response { + s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) err := s.UpdateFolder(c.Params(":uid"), &cmd) if err != nil { return toFolderError(err) @@ -87,7 +87,7 @@ func UpdateFolder(c *models.ReqContext, cmd models.UpdateFolderCommand) response } func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { // temporarily adding this function to HTTPServer, will be removed from HTTPServer when librarypanels featuretoggle is removed - s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) + s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) if hs.Cfg.IsPanelLibraryEnabled() { err := hs.LibraryPanelService.DeleteLibraryPanelsInFolder(c, c.Params(":uid")) if err != nil { diff --git a/pkg/api/folder_permission.go b/pkg/api/folder_permission.go index 21c380e30ae..0b0ca4b31c1 100644 --- a/pkg/api/folder_permission.go +++ b/pkg/api/folder_permission.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/guardian" @@ -14,7 +13,7 @@ import ( ) func (hs *HTTPServer) GetFolderPermissionList(c *models.ReqContext) response.Response { - s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) + s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) folder, err := s.GetFolderByUID(c.Params(":uid")) if err != nil { @@ -62,9 +61,8 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext, apiCmd dtos. return response.Error(400, err.Error(), err) } - s := dashboards.NewFolderService(c.OrgId, c.SignedInUser) + s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) folder, err := s.GetFolderByUID(c.Params(":uid")) - if err != nil { return toFolderError(err) } @@ -79,11 +77,9 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext, apiCmd dtos. return toFolderError(models.ErrFolderAccessDenied) } - cmd := models.UpdateDashboardAclCommand{} - cmd.DashboardID = folder.Id - + var items []*models.DashboardAcl for _, item := range apiCmd.Items { - cmd.Items = append(cmd.Items, &models.DashboardAcl{ + items = append(items, &models.DashboardAcl{ OrgID: c.OrgId, DashboardID: folder.Id, UserID: item.UserID, @@ -99,9 +95,9 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext, apiCmd dtos. if err != nil { return response.Error(500, "Error while retrieving hidden permissions", err) } - cmd.Items = append(cmd.Items, hiddenACL...) + items = append(items, hiddenACL...) - if okToUpdate, err := g.CheckPermissionBeforeUpdate(models.PERMISSION_ADMIN, cmd.Items); err != nil || !okToUpdate { + if okToUpdate, err := g.CheckPermissionBeforeUpdate(models.PERMISSION_ADMIN, items); err != nil || !okToUpdate { if err != nil { if errors.Is(err, guardian.ErrGuardianPermissionExists) || errors.Is(err, guardian.ErrGuardianOverride) { @@ -114,7 +110,7 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext, apiCmd dtos. return response.Error(403, "Cannot remove own admin permission for a folder", nil) } - if err := bus.Dispatch(&cmd); err != nil { + if err := updateDashboardACL(hs, folder.Id, items); err != nil { if errors.Is(err, models.ErrDashboardAclInfoMissing) { err = models.ErrFolderAclInfoMissing } diff --git a/pkg/api/folder_permission_test.go b/pkg/api/folder_permission_test.go index 1a109329255..bcbcbeb4b5a 100644 --- a/pkg/api/folder_permission_test.go +++ b/pkg/api/folder_permission_test.go @@ -49,7 +49,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { routePattern: "/api/folders/:uid/permissions", cmd: cmd, fn: func(sc *scenarioContext) { - callUpdateFolderPermissions(sc) + callUpdateFolderPermissions(t, sc) assert.Equal(t, 404, sc.resp.Code) }, }, hs) @@ -92,7 +92,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { routePattern: "/api/folders/:uid/permissions", cmd: cmd, fn: func(sc *scenarioContext) { - callUpdateFolderPermissions(sc) + callUpdateFolderPermissions(t, sc) assert.Equal(t, 403, sc.resp.Code) }, }, hs) @@ -153,7 +153,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { routePattern: "/api/folders/:uid/permissions", cmd: cmd, fn: func(sc *scenarioContext) { - callUpdateFolderPermissions(sc) + callUpdateFolderPermissions(t, sc) assert.Equal(t, 200, sc.resp.Code) var resp struct { @@ -205,7 +205,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { routePattern: "/api/folders/:uid/permissions", cmd: cmd, fn: func(sc *scenarioContext) { - callUpdateFolderPermissions(sc) + callUpdateFolderPermissions(t, sc) assert.Equal(t, 400, sc.resp.Code) }, }, hs) @@ -233,7 +233,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { routePattern: "/api/folders/:uid/permissions", cmd: cmd, fn: func(sc *scenarioContext) { - callUpdateFolderPermissions(sc) + callUpdateFolderPermissions(t, sc) assert.Equal(t, 400, sc.resp.Code) respJSON, err := jsonMap(sc.resp.Body.Bytes()) require.NoError(t, err) @@ -279,7 +279,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { routePattern: "/api/folders/:uid/permissions", cmd: cmd, fn: func(sc *scenarioContext) { - callUpdateFolderPermissions(sc) + callUpdateFolderPermissions(t, sc) assert.Equal(t, 400, sc.resp.Code) }, }, hs) @@ -355,13 +355,19 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { routePattern: "/api/folders/:uid/permissions", cmd: cmd, fn: func(sc *scenarioContext) { - bus.AddHandler("test", func(cmd *models.UpdateDashboardAclCommand) error { - assert.Len(t, cmd.Items, 4) - return nil + origUpdateDashboardACL := updateDashboardACL + t.Cleanup(func() { + updateDashboardACL = origUpdateDashboardACL }) + var gotItems []*models.DashboardAcl + updateDashboardACL = func(hs *HTTPServer, dashID int64, items []*models.DashboardAcl) error { + gotItems = items + return nil + } sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) + assert.Len(t, gotItems, 4) }, }, hs) }) @@ -372,10 +378,16 @@ func callGetFolderPermissions(sc *scenarioContext, hs *HTTPServer) { sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() } -func callUpdateFolderPermissions(sc *scenarioContext) { - bus.AddHandler("test", func(cmd *models.UpdateDashboardAclCommand) error { - return nil +func callUpdateFolderPermissions(t *testing.T, sc *scenarioContext) { + t.Helper() + + origUpdateDashboardACL := updateDashboardACL + t.Cleanup(func() { + updateDashboardACL = origUpdateDashboardACL }) + updateDashboardACL = func(hs *HTTPServer, dashID int64, items []*models.DashboardAcl) error { + return nil + } sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() } diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index ca7ec90fb36..c83fd8760e5 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/bus" + dboards "github.com/grafana/grafana/pkg/dashboards" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/setting" @@ -174,12 +175,16 @@ func updateFolderScenario(t *testing.T, desc string, url string, routePattern st t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { defer bus.ClearBusHandlers() + hs := HTTPServer{ + Cfg: setting.NewCfg(), + } + sc := setupScenarioContext(t, url) sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { sc.context = c sc.context.SignedInUser = &models.SignedInUser{OrgId: testOrgID, UserId: testUserID} - return UpdateFolder(c, cmd) + return hs.UpdateFolder(c, cmd) }) origNewFolderService := dashboards.NewFolderService @@ -195,6 +200,8 @@ func updateFolderScenario(t *testing.T, desc string, url string, routePattern st } type fakeFolderService struct { + dashboards.FolderService + GetFoldersResult []*models.Folder GetFoldersError error GetFolderByUIDResult *models.Folder @@ -222,9 +229,8 @@ func (s *fakeFolderService) GetFolderByUID(uid string) (*models.Folder, error) { return s.GetFolderByUIDResult, s.GetFolderByUIDError } -func (s *fakeFolderService) CreateFolder(cmd *models.CreateFolderCommand) error { - cmd.Result = s.CreateFolderResult - return s.CreateFolderError +func (s *fakeFolderService) CreateFolder(title, uid string) (*models.Folder, error) { + return s.CreateFolderResult, s.CreateFolderError } func (s *fakeFolderService) UpdateFolder(existingUID string, cmd *models.UpdateFolderCommand) error { @@ -238,7 +244,8 @@ func (s *fakeFolderService) DeleteFolder(uid string) (*models.Folder, error) { } func mockFolderService(mock *fakeFolderService) { - dashboards.NewFolderService = func(orgId int64, user *models.SignedInUser) dashboards.FolderService { + dashboards.NewFolderService = func(orgId int64, user *models.SignedInUser, + dashboardStore dboards.Store) dashboards.FolderService { return mock } } diff --git a/pkg/api/frontendlogging/source_maps.go b/pkg/api/frontendlogging/source_maps.go index bc3579a18fa..88ae3615be5 100644 --- a/pkg/api/frontendlogging/source_maps.go +++ b/pkg/api/frontendlogging/source_maps.go @@ -43,10 +43,10 @@ func ReadSourceMapFromFS(dir string, path string) ([]byte, error) { } type SourceMapStore struct { + sync.Mutex cache map[string]*sourceMap cfg *setting.Cfg readSourceMap ReadSourceMapFn - sync.Mutex } func NewSourceMapStore(cfg *setting.Cfg, readSourceMap ReadSourceMapFn) *SourceMapStore { diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index c647373edcf..137242a4437 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -5,7 +5,6 @@ import ( "strconv" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/util" @@ -110,12 +109,12 @@ func (hs *HTTPServer) getFSDataSources(c *models.ReqContext, enabledPlugins *plu // add data sources that are built in (meaning they are not added via data sources page, nor have any entry in // the datasource table) - for _, ds := range manager.DataSources { + for _, ds := range hs.PluginManager.DataSources() { if ds.BuiltIn { dataSources[ds.Name] = map[string]interface{}{ "type": ds.Type, "name": ds.Name, - "meta": manager.DataSources[ds.Id], + "meta": hs.PluginManager.GetDataSource(ds.Id), } } } @@ -226,8 +225,8 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i "commit": commit, "buildstamp": buildstamp, "edition": hs.License.Edition(), - "latestVersion": hs.PluginManager.GrafanaLatestVersion, - "hasUpdate": hs.PluginManager.GrafanaHasUpdate, + "latestVersion": hs.PluginManager.GrafanaLatestVersion(), + "hasUpdate": hs.PluginManager.GrafanaHasUpdate(), "env": setting.Env, "isEnterprise": hs.License.HasValidLicense(), }, diff --git a/pkg/api/frontendsettings_test.go b/pkg/api/frontendsettings_test.go index b25210dfda2..bd9b4dfcbea 100644 --- a/pkg/api/frontendsettings_test.go +++ b/pkg/api/frontendsettings_test.go @@ -39,18 +39,21 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg) (*macaron.Macaron, *HT }) } - bus.ClearBusHandlers() - bus.AddHandler("sql", sqlstore.GetPluginSettings) - t.Cleanup(bus.ClearBusHandlers) + sqlStore := sqlstore.InitTestDB(t) + pm := &manager.PluginManager{Cfg: cfg, SQLStore: sqlStore} - r := &rendering.RenderingService{Cfg: cfg} + r := &rendering.RenderingService{ + Cfg: cfg, + PluginManager: pm, + } hs := &HTTPServer{ Cfg: cfg, Bus: bus.GetBus(), License: &licensing.OSSLicensingService{Cfg: cfg}, RenderService: r, - PluginManager: &manager.PluginManager{Cfg: cfg}, + SQLStore: sqlStore, + PluginManager: pm, } m := macaron.New() diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 436159a429a..80776e26ac0 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -13,6 +13,7 @@ import ( "strings" "sync" + "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/live" "github.com/grafana/grafana/pkg/services/search" @@ -79,7 +80,7 @@ type HTTPServer struct { License models.Licensing `inject:""` BackendPluginManager backendplugin.Manager `inject:""` PluginRequestValidator models.PluginRequestValidator `inject:""` - PluginManager *manager.PluginManager `inject:""` + PluginManager plugins.Manager `inject:""` SearchService *search.SearchService `inject:""` ShortURLService *shorturls.ShortURLService `inject:""` Live *live.GrafanaLive `inject:""` diff --git a/pkg/api/index.go b/pkg/api/index.go index e97b6e075ab..a87de4c3a89 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -400,8 +400,8 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat GoogleTagManagerId: setting.GoogleTagManagerId, BuildVersion: setting.BuildVersion, BuildCommit: setting.BuildCommit, - NewGrafanaVersion: hs.PluginManager.GrafanaLatestVersion, - NewGrafanaVersionExists: hs.PluginManager.GrafanaHasUpdate, + NewGrafanaVersion: hs.PluginManager.GrafanaLatestVersion(), + NewGrafanaVersionExists: hs.PluginManager.GrafanaHasUpdate(), AppName: setting.ApplicationName, AppNameBodyClass: getAppNameBodyClass(hs.License.HasValidLicense()), FavIcon: "public/img/fav32.png", diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index bf5ef1bb284..65de498ad71 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -25,8 +25,8 @@ var ErrPluginNotFound error = errors.New("plugin not found, no installed plugin func (hs *HTTPServer) getPluginContext(pluginID string, user *models.SignedInUser) (backend.PluginContext, error) { pc := backend.PluginContext{} - plugin, exists := manager.Plugins[pluginID] - if !exists { + plugin := hs.PluginManager.GetPlugin(pluginID) + if plugin == nil { return pc, ErrPluginNotFound } @@ -74,13 +74,12 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response { } pluginSettingsMap, err := hs.PluginManager.GetPluginSettings(c.OrgId) - if err != nil { return response.Error(500, "Failed to get list of plugins", err) } result := make(dtos.PluginList, 0) - for _, pluginDef := range manager.Plugins { + for _, pluginDef := range hs.PluginManager.Plugins() { // filter out app sub plugins if embeddedFilter == "0" && pluginDef.IncludedInAppId != "" { continue @@ -130,7 +129,7 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response { } // filter out built in data sources - if ds, exists := manager.DataSources[pluginDef.Id]; exists { + if ds := hs.PluginManager.GetDataSource(pluginDef.Id); ds != nil { if ds.BuiltIn { continue } @@ -143,11 +142,11 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response { return response.JSON(200, result) } -func GetPluginSettingByID(c *models.ReqContext) response.Response { +func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Response { pluginID := c.Params(":pluginId") - def, exists := manager.Plugins[pluginID] - if !exists { + def := hs.PluginManager.GetPlugin(pluginID) + if def == nil { return response.Error(404, "Plugin not found, no installed plugin with that id", nil) } @@ -256,7 +255,7 @@ func (hs *HTTPServer) ImportDashboard(c *models.ReqContext, apiCmd dtos.ImportDa dashInfo, err := hs.PluginManager.ImportDashboard(apiCmd.PluginId, apiCmd.Path, c.OrgId, apiCmd.FolderId, apiCmd.Dashboard, apiCmd.Overwrite, apiCmd.Inputs, c.SignedInUser, hs.DataService) if err != nil { - return dashboardSaveErrorToApiResponse(err) + return hs.dashboardSaveErrorToApiResponse(err) } return response.JSON(200, dashInfo) @@ -267,8 +266,8 @@ func (hs *HTTPServer) ImportDashboard(c *models.ReqContext, apiCmd dtos.ImportDa // /api/plugins/:pluginId/metrics func (hs *HTTPServer) CollectPluginMetrics(c *models.ReqContext) response.Response { pluginID := c.Params("pluginId") - plugin, exists := manager.Plugins[pluginID] - if !exists { + plugin := hs.PluginManager.GetPlugin(pluginID) + if plugin == nil { return response.Error(404, "Plugin not found", nil) } diff --git a/pkg/api/team.go b/pkg/api/team.go index e475724e077..258a84797c3 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -7,19 +7,19 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/teamguardian" "github.com/grafana/grafana/pkg/util" ) // POST /api/teams func (hs *HTTPServer) CreateTeam(c *models.ReqContext, cmd models.CreateTeamCommand) response.Response { - cmd.OrgId = c.OrgId - if c.OrgRole == models.ROLE_VIEWER { return response.Error(403, "Not allowed to create team.", nil) } - if err := hs.Bus.Dispatch(&cmd); err != nil { + team, err := createTeam(hs.SQLStore, cmd.Name, cmd.Email, c.OrgId) + if err != nil { if errors.Is(err, models.ErrTeamNameTaken) { return response.Error(409, "Team name taken", err) } @@ -31,23 +31,17 @@ func (hs *HTTPServer) CreateTeam(c *models.ReqContext, cmd models.CreateTeamComm // the SignedInUser is an empty struct therefore // an additional check whether it is an actual user is required if c.SignedInUser.IsRealUser() { - addMemberCmd := models.AddTeamMemberCommand{ - UserId: c.SignedInUser.UserId, - OrgId: cmd.OrgId, - TeamId: cmd.Result.Id, - Permission: models.PERMISSION_ADMIN, - } - - if err := hs.Bus.Dispatch(&addMemberCmd); err != nil { - c.Logger.Error("Could not add creator to team.", "error", err) + if err := addTeamMember(hs.SQLStore, c.SignedInUser.UserId, c.OrgId, team.Id, false, + models.PERMISSION_ADMIN); err != nil { + c.Logger.Error("Could not add creator to team", "error", err) } } else { - c.Logger.Warn("Could not add creator to team because is not a real user.") + c.Logger.Warn("Could not add creator to team because is not a real user") } } return response.JSON(200, &util.DynMap{ - "teamId": cmd.Result.Id, + "teamId": team.Id, "message": "Team created", }) } @@ -175,3 +169,18 @@ func (hs *HTTPServer) UpdateTeamPreferences(c *models.ReqContext, dtoCmd dtos.Up return updatePreferencesFor(orgId, 0, teamId, &dtoCmd) } + +// createTeam creates a team. +// +// Stubbable by tests. +var createTeam = func(sqlStore *sqlstore.SQLStore, name, email string, orgID int64) (models.Team, error) { + return sqlStore.CreateTeam(name, email, orgID) +} + +// addTeamMember adds a team member. +// +// Stubbable by tests. +var addTeamMember = func(sqlStore *sqlstore.SQLStore, userID, orgID, teamID int64, isExternal bool, + permission models.PermissionType) error { + return sqlStore.AddTeamMember(userID, orgID, teamID, isExternal, permission) +} diff --git a/pkg/api/team_test.go b/pkg/api/team_test.go index c94aeaea3ec..3bf1473dc4d 100644 --- a/pkg/api/team_test.go +++ b/pkg/api/team_test.go @@ -3,6 +3,7 @@ package api import ( "testing" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" macaron "gopkg.in/macaron.v1" @@ -98,20 +99,29 @@ func TestTeamAPIEndpoint(t *testing.T) { teamName := "team foo" - createTeamCalled := 0 - bus.AddHandler("test", func(cmd *models.CreateTeamCommand) error { - createTeamCalled += 1 - cmd.Result = models.Team{Name: teamName, Id: 42} - return nil + // TODO: Use a fake SQLStore when it's represented by an interface + origCreateTeam := createTeam + origAddTeamMember := addTeamMember + t.Cleanup(func() { + createTeam = origCreateTeam + addTeamMember = origAddTeamMember }) + createTeamCalled := 0 + createTeam = func(sqlStore *sqlstore.SQLStore, name, email string, orgID int64) (models.Team, error) { + createTeamCalled++ + return models.Team{Name: teamName, Id: 42}, nil + } + addTeamMemberCalled := 0 - bus.AddHandler("test", func(cmd *models.AddTeamMemberCommand) error { - addTeamMemberCalled += 1 + addTeamMember = func(sqlStore *sqlstore.SQLStore, userID, orgID, teamID int64, isExternal bool, + permission models.PermissionType) error { + addTeamMemberCalled++ return nil - }) + } - req, _ := http.NewRequest("POST", "/api/teams", nil) + req, err := http.NewRequest("POST", "/api/teams", nil) + require.NoError(t, err) t.Run("with no real signed in user", func(t *testing.T) { stub := &testLogger{} @@ -128,7 +138,7 @@ func TestTeamAPIEndpoint(t *testing.T) { assert.Equal(t, createTeamCalled, 1) assert.Equal(t, addTeamMemberCalled, 0) assert.True(t, stub.warnCalled) - assert.Equal(t, stub.warnMessage, "Could not add creator to team because is not a real user.") + assert.Equal(t, stub.warnMessage, "Could not add creator to team because is not a real user") }) t.Run("with real signed in user", func(t *testing.T) { diff --git a/pkg/dashboards/ifaces.go b/pkg/dashboards/ifaces.go new file mode 100644 index 00000000000..4699a6d9c16 --- /dev/null +++ b/pkg/dashboards/ifaces.go @@ -0,0 +1,16 @@ +package dashboards + +import "github.com/grafana/grafana/pkg/models" + +// Store is a dashboard store. +type Store interface { + // ValidateDashboardBeforeSave validates a dashboard before save. + ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error) + GetProvisionedDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) + GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) + SaveProvisionedDashboard(cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) + SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error) + UpdateDashboardACL(uid int64, items []*models.DashboardAcl) error + // SaveAlerts saves dashboard alerts. + SaveAlerts(dashID int64, alerts []*models.Alert) error +} diff --git a/pkg/infra/usagestats/service.go b/pkg/infra/usagestats/service.go index 79b500f0a34..ffe22d3ffe1 100644 --- a/pkg/infra/usagestats/service.go +++ b/pkg/infra/usagestats/service.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -38,6 +39,7 @@ type UsageStatsService struct { SQLStore *sqlstore.SQLStore `inject:""` AlertingUsageStats alerting.UsageStatsQuerier `inject:""` License models.Licensing `inject:""` + PluginManager plugins.Manager `inject:""` log log.Logger diff --git a/pkg/infra/usagestats/usage_stats.go b/pkg/infra/usagestats/usage_stats.go index fabddfb0431..a22da57eb56 100644 --- a/pkg/infra/usagestats/usage_stats.go +++ b/pkg/infra/usagestats/usage_stats.go @@ -58,7 +58,7 @@ func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport, metrics["stats.playlist.count"] = statsQuery.Result.Playlists metrics["stats.plugins.apps.count"] = len(manager.Apps) metrics["stats.plugins.panels.count"] = len(manager.Panels) - metrics["stats.plugins.datasources.count"] = len(manager.DataSources) + metrics["stats.plugins.datasources.count"] = uss.PluginManager.DataSourceCount() metrics["stats.alerts.count"] = statsQuery.Result.Alerts metrics["stats.active_users.count"] = statsQuery.Result.ActiveUsers metrics["stats.datasources.count"] = statsQuery.Result.Datasources @@ -329,8 +329,8 @@ func (uss *UsageStatsService) updateTotalStats() { } func (uss *UsageStatsService) shouldBeReported(dsType string) bool { - ds, ok := manager.DataSources[dsType] - if !ok { + ds := uss.PluginManager.GetDataSource(dsType) + if ds == nil { return false } diff --git a/pkg/infra/usagestats/usage_stats_test.go b/pkg/infra/usagestats/usage_stats_test.go index f786da847d3..2df3829e012 100644 --- a/pkg/infra/usagestats/usage_stats_test.go +++ b/pkg/infra/usagestats/usage_stats_test.go @@ -296,7 +296,7 @@ func TestMetrics(t *testing.T) { assert.Equal(t, getSystemStatsQuery.Result.Playlists, metrics.Get("stats.playlist.count").MustInt64()) assert.Equal(t, len(manager.Apps), metrics.Get("stats.plugins.apps.count").MustInt()) assert.Equal(t, len(manager.Panels), metrics.Get("stats.plugins.panels.count").MustInt()) - assert.Equal(t, len(manager.DataSources), metrics.Get("stats.plugins.datasources.count").MustInt()) + assert.Equal(t, uss.PluginManager.DataSourceCount(), metrics.Get("stats.plugins.datasources.count").MustInt()) assert.Equal(t, getSystemStatsQuery.Result.Alerts, metrics.Get("stats.alerts.count").MustInt64()) assert.Equal(t, getSystemStatsQuery.Result.ActiveUsers, metrics.Get("stats.active_users.count").MustInt64()) assert.Equal(t, getSystemStatsQuery.Result.Datasources, metrics.Get("stats.datasources.count").MustInt64()) @@ -542,37 +542,51 @@ func (aum *alertingUsageMock) QueryUsageStats() (*alerting.UsageStats, error) { }, nil } +type fakePluginManager struct { + manager.PluginManager + + dataSources map[string]*plugins.DataSourcePlugin +} + +func (pm fakePluginManager) DataSourceCount() int { + return len(pm.dataSources) +} + +func (pm fakePluginManager) GetDataSource(id string) *plugins.DataSourcePlugin { + return pm.dataSources[id] +} + func setupSomeDataSourcePlugins(t *testing.T, uss *UsageStatsService) { t.Helper() - originalDataSources := manager.DataSources - t.Cleanup(func() { manager.DataSources = originalDataSources }) - manager.DataSources = map[string]*plugins.DataSourcePlugin{ - models.DS_ES: { - FrontendPluginBase: plugins.FrontendPluginBase{ - PluginBase: plugins.PluginBase{ - Signature: "internal", + uss.PluginManager = &fakePluginManager{ + dataSources: map[string]*plugins.DataSourcePlugin{ + models.DS_ES: { + FrontendPluginBase: plugins.FrontendPluginBase{ + PluginBase: plugins.PluginBase{ + Signature: "internal", + }, }, }, - }, - models.DS_PROMETHEUS: { - FrontendPluginBase: plugins.FrontendPluginBase{ - PluginBase: plugins.PluginBase{ - Signature: "internal", + models.DS_PROMETHEUS: { + FrontendPluginBase: plugins.FrontendPluginBase{ + PluginBase: plugins.PluginBase{ + Signature: "internal", + }, }, }, - }, - models.DS_GRAPHITE: { - FrontendPluginBase: plugins.FrontendPluginBase{ - PluginBase: plugins.PluginBase{ - Signature: "internal", + models.DS_GRAPHITE: { + FrontendPluginBase: plugins.FrontendPluginBase{ + PluginBase: plugins.PluginBase{ + Signature: "internal", + }, }, }, - }, - models.DS_MYSQL: { - FrontendPluginBase: plugins.FrontendPluginBase{ - PluginBase: plugins.PluginBase{ - Signature: "internal", + models.DS_MYSQL: { + FrontendPluginBase: plugins.FrontendPluginBase{ + PluginBase: plugins.PluginBase{ + Signature: "internal", + }, }, }, }, @@ -595,5 +609,6 @@ func createService(t *testing.T, cfg setting.Cfg) *UsageStatsService { License: &licensing.OSSLicensingService{}, AlertingUsageStats: &alertingUsageMock{}, externalMetrics: make(map[string]MetricFunc), + PluginManager: &fakePluginManager{}, } } diff --git a/pkg/models/alert.go b/pkg/models/alert.go index f62b1012520..b4da89d143c 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -209,18 +209,3 @@ type AlertStateInfoDTO struct { State AlertStateType `json:"state"` NewStateDate time.Time `json:"newStateDate"` } - -// "Internal" commands - -type UpdateDashboardAlertsCommand struct { - OrgId int64 - Dashboard *Dashboard - User *SignedInUser -} - -type ValidateDashboardAlertsCommand struct { - UserId int64 - OrgId int64 - Dashboard *Dashboard - User *SignedInUser -} diff --git a/pkg/models/dashboard_acl.go b/pkg/models/dashboard_acl.go index 90ebfa250e3..f1fb2ef17d1 100644 --- a/pkg/models/dashboard_acl.go +++ b/pkg/models/dashboard_acl.go @@ -95,15 +95,6 @@ func (dto *DashboardAclInfoDTO) IsDuplicateOf(other *DashboardAclInfoDTO) bool { return dto.hasSameRoleAs(other) || dto.hasSameUserAs(other) || dto.hasSameTeamAs(other) } -// -// COMMANDS -// - -type UpdateDashboardAclCommand struct { - DashboardID int64 - Items []*DashboardAcl -} - // // QUERIES // diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 222a7aa2625..c3281897743 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -367,13 +367,6 @@ type DeleteDashboardCommand struct { OrgId int64 } -type ValidateDashboardBeforeSaveCommand struct { - OrgId int64 - Dashboard *Dashboard - Overwrite bool - Result *ValidateDashboardBeforeSaveResult -} - type DeleteOrphanedProvisionedDashboardsCommand struct { ReaderNames []string } @@ -430,11 +423,6 @@ type GetProvisionedDashboardDataByIdQuery struct { Result *DashboardProvisioning } -type GetProvisionedDashboardDataQuery struct { - Name string - Result []*DashboardProvisioning -} - type GetDashboardsBySlugQuery struct { OrgId int64 Slug string diff --git a/pkg/models/folders.go b/pkg/models/folders.go index f3aac27055e..09ff05b657c 100644 --- a/pkg/models/folders.go +++ b/pkg/models/folders.go @@ -32,23 +32,6 @@ type Folder struct { HasAcl bool } -// GetDashboardModel turns the command into the saveable model -func (cmd *CreateFolderCommand) GetDashboardModel(orgId int64, userId int64) *Dashboard { - dashFolder := NewDashboardFolder(strings.TrimSpace(cmd.Title)) - dashFolder.OrgId = orgId - dashFolder.SetUid(strings.TrimSpace(cmd.Uid)) - - if userId == 0 { - userId = -1 - } - - dashFolder.CreatedBy = userId - dashFolder.UpdatedBy = userId - dashFolder.UpdateSlug() - - return dashFolder -} - // UpdateDashboardModel updates an existing model from command into model for update func (cmd *UpdateFolderCommand) UpdateDashboardModel(dashFolder *Dashboard, orgId int64, userId int64) { dashFolder.OrgId = orgId diff --git a/pkg/models/plugin_settings.go b/pkg/models/plugin_settings.go index 9307612ccba..97996f8867d 100644 --- a/pkg/models/plugin_settings.go +++ b/pkg/models/plugin_settings.go @@ -53,10 +53,6 @@ func (cmd *UpdatePluginSettingCmd) GetEncryptedJsonData() securejsondata.SecureJ // --------------------- // QUERIES -type GetPluginSettingsQuery struct { - OrgId int64 - Result []*PluginSettingInfoDTO -} type PluginSettingInfoDTO struct { OrgId int64 diff --git a/pkg/plugins/ifaces.go b/pkg/plugins/ifaces.go index 981c1c7db7f..72e03246f88 100644 --- a/pkg/plugins/ifaces.go +++ b/pkg/plugins/ifaces.go @@ -3,10 +3,55 @@ package plugins import ( "context" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" ) +// Manager is the plugin manager service interface. +type Manager interface { + // Renderer gets the renderer plugin. + Renderer() *RendererPlugin + // GetDataSource gets a data source plugin with a certain ID. + GetDataSource(id string) *DataSourcePlugin + // GetDataPlugin gets a data plugin with a certain ID. + GetDataPlugin(id string) DataPlugin + // GetPlugin gets a plugin with a certain ID. + GetPlugin(id string) *PluginBase + // DataSourceCount gets the number of data sources. + DataSourceCount() int + // DataSources gets all data sources. + DataSources() []*DataSourcePlugin + // GetEnabledPlugins gets enabled plugins. + GetEnabledPlugins(orgID int64) (*EnabledPlugins, error) + // GrafanaLatestVersion gets the latest Grafana version. + GrafanaLatestVersion() string + // GrafanaHasUpdate returns whether Grafana has an update. + GrafanaHasUpdate() bool + // Plugins gets all plugins. + Plugins() []*PluginBase + // GetPluginSettings gets settings for a certain plugin. + GetPluginSettings(orgID int64) (map[string]*models.PluginSettingInfoDTO, error) + // GetPluginDashboards gets dashboards for a certain org/plugin. + GetPluginDashboards(orgID int64, pluginID string) ([]*PluginDashboardInfoDTO, error) + // GetPluginMarkdown gets markdown for a certain plugin/name. + GetPluginMarkdown(pluginID string, name string) ([]byte, error) + // ImportDashboard imports a dashboard. + ImportDashboard(pluginID, path string, orgID, folderID int64, dashboardModel *simplejson.Json, + overwrite bool, inputs []ImportDashboardInput, user *models.SignedInUser, + requestHandler DataRequestHandler) (PluginDashboardInfoDTO, error) + // ScanningErrors returns plugin scanning errors encountered. + ScanningErrors() []PluginError +} + +type ImportDashboardInput struct { + Type string `json:"type"` + PluginId string `json:"pluginId"` + Name string `json:"name"` + Value string `json:"value"` +} + // DataRequestHandler is a data request handler interface. type DataRequestHandler interface { + // HandleRequest handles a data request. HandleRequest(context.Context, *models.DataSource, DataQuery) (DataResponse, error) } diff --git a/pkg/plugins/manager/dashboard_import.go b/pkg/plugins/manager/dashboard_import.go index 97a66f4d517..c6debf67021 100644 --- a/pkg/plugins/manager/dashboard_import.go +++ b/pkg/plugins/manager/dashboard_import.go @@ -13,13 +13,6 @@ import ( var varRegex = regexp.MustCompile(`(\$\{.+?\})`) -type ImportDashboardInput struct { - Type string `json:"type"` - PluginId string `json:"pluginId"` - Name string `json:"name"` - Value string `json:"value"` -} - type DashboardInputMissingError struct { VariableName string } @@ -29,7 +22,7 @@ func (e DashboardInputMissingError) Error() string { } func (pm *PluginManager) ImportDashboard(pluginID, path string, orgID, folderID int64, dashboardModel *simplejson.Json, - overwrite bool, inputs []ImportDashboardInput, user *models.SignedInUser, + overwrite bool, inputs []plugins.ImportDashboardInput, user *models.SignedInUser, requestHandler plugins.DataRequestHandler) (plugins.PluginDashboardInfoDTO, error) { var dashboard *models.Dashboard if pluginID != "" { @@ -67,7 +60,7 @@ func (pm *PluginManager) ImportDashboard(pluginID, path string, orgID, folderID User: user, } - savedDash, err := dashboards.NewService().ImportDashboard(dto) + savedDash, err := dashboards.NewService(pm.SQLStore).ImportDashboard(dto) if err != nil { return plugins.PluginDashboardInfoDTO{}, err } @@ -89,12 +82,12 @@ func (pm *PluginManager) ImportDashboard(pluginID, path string, orgID, folderID type DashTemplateEvaluator struct { template *simplejson.Json - inputs []ImportDashboardInput + inputs []plugins.ImportDashboardInput variables map[string]string result *simplejson.Json } -func (e *DashTemplateEvaluator) findInput(varName string, varType string) *ImportDashboardInput { +func (e *DashTemplateEvaluator) findInput(varName string, varType string) *plugins.ImportDashboardInput { for _, input := range e.inputs { if varType == input.Type && (input.Name == varName || input.Name == "*") { return &input diff --git a/pkg/plugins/manager/dashboard_import_test.go b/pkg/plugins/manager/dashboard_import_test.go index ea4a1c92b96..0dafdec0df1 100644 --- a/pkg/plugins/manager/dashboard_import_test.go +++ b/pkg/plugins/manager/dashboard_import_test.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/require" @@ -21,7 +22,7 @@ func TestDashboardImport(t *testing.T) { dashboards.MockDashboardService(mock) info, err := pm.ImportDashboard("test-app", "dashboards/connections.json", 1, 0, nil, false, - []ImportDashboardInput{ + []plugins.ImportDashboardInput{ {Name: "*", Type: "datasource", Value: "graphite"}, }, &models.SignedInUser{UserId: 1, OrgRole: models.ROLE_ADMIN}, nil) require.NoError(t, err) @@ -58,7 +59,7 @@ func TestDashboardImport(t *testing.T) { evaluator := &DashTemplateEvaluator{ template: template, - inputs: []ImportDashboardInput{ + inputs: []plugins.ImportDashboardInput{ {Name: "*", Type: "datasource", Value: "my-server"}, }, } diff --git a/pkg/plugins/manager/dashboards.go b/pkg/plugins/manager/dashboards.go index 4a6de3baf84..3829abd7943 100644 --- a/pkg/plugins/manager/dashboards.go +++ b/pkg/plugins/manager/dashboards.go @@ -11,7 +11,7 @@ import ( ) func (pm *PluginManager) GetPluginDashboards(orgId int64, pluginId string) ([]*plugins.PluginDashboardInfoDTO, error) { - plugin, exists := Plugins[pluginId] + plugin, exists := pm.plugins[pluginId] if !exists { return nil, plugins.PluginNotFoundError{PluginID: pluginId} } @@ -71,7 +71,7 @@ func (pm *PluginManager) GetPluginDashboards(orgId int64, pluginId string) ([]*p } func (pm *PluginManager) LoadPluginDashboard(pluginId, path string) (*models.Dashboard, error) { - plugin, exists := Plugins[pluginId] + plugin, exists := pm.plugins[pluginId] if !exists { return nil, plugins.PluginNotFoundError{PluginID: pluginId} } diff --git a/pkg/plugins/manager/manager.go b/pkg/plugins/manager/manager.go index 903c979c3f8..07fbd7e5456 100644 --- a/pkg/plugins/manager/manager.go +++ b/pkg/plugins/manager/manager.go @@ -21,19 +21,17 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/errutil" ) var ( - DataSources map[string]*plugins.DataSourcePlugin Panels map[string]*plugins.PanelPlugin StaticRoutes []*plugins.PluginStaticRoute Apps map[string]*plugins.AppPlugin - Plugins map[string]*plugins.PluginBase PluginTypes map[string]interface{} - Renderer *plugins.RendererPlugin plog log.Logger ) @@ -54,30 +52,36 @@ type PluginScanner struct { type PluginManager struct { BackendPluginManager backendplugin.Manager `inject:""` Cfg *setting.Cfg `inject:""` + SQLStore *sqlstore.SQLStore `inject:""` log log.Logger scanningErrors []error // AllowUnsignedPluginsCondition changes the policy for allowing unsigned plugins. Signature validation only runs when plugins are starting // and running plugins will not be terminated if they violate the new policy. AllowUnsignedPluginsCondition unsignedPluginConditionFunc - GrafanaLatestVersion string - GrafanaHasUpdate bool + grafanaLatestVersion string + grafanaHasUpdate bool pluginScanningErrors map[string]plugins.PluginError + + renderer *plugins.RendererPlugin + dataSources map[string]*plugins.DataSourcePlugin + plugins map[string]*plugins.PluginBase } func init() { - registry.RegisterService(&PluginManager{}) + registry.RegisterService(&PluginManager{ + dataSources: map[string]*plugins.DataSourcePlugin{}, + }) } func (pm *PluginManager) Init() error { pm.log = log.New("plugins") plog = log.New("plugins") - DataSources = map[string]*plugins.DataSourcePlugin{} StaticRoutes = []*plugins.PluginStaticRoute{} Panels = map[string]*plugins.PanelPlugin{} Apps = map[string]*plugins.AppPlugin{} - Plugins = map[string]*plugins.PluginBase{} + pm.plugins = map[string]*plugins.PluginBase{} PluginTypes = map[string]interface{}{ "panel": plugins.PanelPlugin{}, "datasource": plugins.DataSourcePlugin{}, @@ -134,22 +138,22 @@ func (pm *PluginManager) Init() error { StaticRoutes = append(StaticRoutes, staticRoutes...) } - for _, ds := range DataSources { + for _, ds := range pm.dataSources { staticRoutes := ds.InitFrontendPlugin(pm.Cfg) StaticRoutes = append(StaticRoutes, staticRoutes...) } for _, app := range Apps { - staticRoutes := app.InitApp(Panels, DataSources, pm.Cfg) + staticRoutes := app.InitApp(Panels, pm.dataSources, pm.Cfg) StaticRoutes = append(StaticRoutes, staticRoutes...) } - if Renderer != nil { - staticRoutes := Renderer.InitFrontendPlugin(pm.Cfg) + if pm.renderer != nil { + staticRoutes := pm.renderer.InitFrontendPlugin(pm.Cfg) StaticRoutes = append(StaticRoutes, staticRoutes...) } - for _, p := range Plugins { + for _, p := range pm.plugins { if p.IsCorePlugin { p.Signature = plugins.PluginSignatureInternal } else { @@ -178,6 +182,48 @@ func (pm *PluginManager) Run(ctx context.Context) error { return ctx.Err() } +func (pm *PluginManager) Renderer() *plugins.RendererPlugin { + return pm.renderer +} + +func (pm *PluginManager) GetDataSource(id string) *plugins.DataSourcePlugin { + return pm.dataSources[id] +} + +func (pm *PluginManager) DataSources() []*plugins.DataSourcePlugin { + var rslt []*plugins.DataSourcePlugin + for _, ds := range pm.dataSources { + rslt = append(rslt, ds) + } + + return rslt +} + +func (pm *PluginManager) DataSourceCount() int { + return len(pm.dataSources) +} + +func (pm *PluginManager) Plugins() []*plugins.PluginBase { + var rslt []*plugins.PluginBase + for _, p := range pm.plugins { + rslt = append(rslt, p) + } + + return rslt +} + +func (pm *PluginManager) GetPlugin(id string) *plugins.PluginBase { + return pm.plugins[id] +} + +func (pm *PluginManager) GrafanaLatestVersion() string { + return pm.grafanaLatestVersion +} + +func (pm *PluginManager) GrafanaHasUpdate() bool { + return pm.grafanaHasUpdate +} + // scanPluginPaths scans configured plugin paths. func (pm *PluginManager) scanPluginPaths() error { for pluginID, settings := range pm.Cfg.PluginSettings { @@ -315,13 +361,13 @@ func (pm *PluginManager) loadPlugin(jsonParser *json.Decoder, pluginBase *plugin var pb *plugins.PluginBase switch p := plug.(type) { case *plugins.DataSourcePlugin: - DataSources[p.Id] = p + pm.dataSources[p.Id] = p pb = &p.PluginBase case *plugins.PanelPlugin: Panels[p.Id] = p pb = &p.PluginBase case *plugins.RendererPlugin: - Renderer = p + pm.renderer = p pb = &p.PluginBase case *plugins.AppPlugin: Apps[p.Id] = p @@ -330,7 +376,7 @@ func (pm *PluginManager) loadPlugin(jsonParser *json.Decoder, pluginBase *plugin panic(fmt.Sprintf("Unrecognized plugin type %T", plug)) } - if p, exists := Plugins[pb.Id]; exists { + if p, exists := pm.plugins[pb.Id]; exists { pm.log.Warn("Plugin is duplicate", "id", pb.Id) scanner.errors = append(scanner.errors, plugins.DuplicatePluginError{Plugin: pb, ExistingPlugin: p}) return nil @@ -360,20 +406,11 @@ func (pm *PluginManager) loadPlugin(jsonParser *json.Decoder, pluginBase *plugin pb.SignatureType = pluginBase.SignatureType pb.SignatureOrg = pluginBase.SignatureOrg - Plugins[pb.Id] = pb + pm.plugins[pb.Id] = pb pm.log.Debug("Successfully added plugin", "id", pb.Id) return nil } -// GetDatasource returns a datasource based on passed pluginID if it exists -// -// This function fetches the datasource from the global variable DataSources in this package. -// Rather then refactor all dependencies on the global variable we can use this as an transition. -func (pm *PluginManager) GetDatasource(pluginID string) (*plugins.DataSourcePlugin, bool) { - ds, exists := DataSources[pluginID] - return ds, exists -} - func (s *PluginScanner) walker(currentPath string, f os.FileInfo, err error) error { // We scan all the subfolders for plugin.json (with some exceptions) so that we also load embedded plugins, for // example https://github.com/raintank/worldping-app/tree/master/dist/grafana-worldmap-panel worldmap panel plugin @@ -545,7 +582,7 @@ func (pm *PluginManager) ScanningErrors() []plugins.PluginError { } func (pm *PluginManager) GetPluginMarkdown(pluginId string, name string) ([]byte, error) { - plug, exists := Plugins[pluginId] + plug, exists := pm.plugins[pluginId] if !exists { return nil, plugins.PluginNotFoundError{PluginID: pluginId} } @@ -600,14 +637,14 @@ func collectPluginFilesWithin(rootDir string) ([]string, error) { } // GetDataPlugin gets a DataPlugin with a certain name. If none is found, nil is returned. -func (pm *PluginManager) GetDataPlugin(pluginID string) plugins.DataPlugin { - if p, exists := DataSources[pluginID]; exists && p.CanHandleDataQueries() { +func (pm *PluginManager) GetDataPlugin(id string) plugins.DataPlugin { + if p, exists := pm.dataSources[id]; exists && p.CanHandleDataQueries() { return p } // XXX: Might other plugins implement DataPlugin? - p := pm.BackendPluginManager.GetDataPlugin(pluginID) + p := pm.BackendPluginManager.GetDataPlugin(id) if p != nil { return p.(plugins.DataPlugin) } diff --git a/pkg/plugins/manager/manager_test.go b/pkg/plugins/manager/manager_test.go index 871f98943f9..c1e10a08b37 100644 --- a/pkg/plugins/manager/manager_test.go +++ b/pkg/plugins/manager/manager_test.go @@ -30,9 +30,9 @@ func TestPluginManager_Init(t *testing.T) { require.NoError(t, err) assert.Empty(t, pm.scanningErrors) - assert.Greater(t, len(DataSources), 1) + assert.Greater(t, len(pm.dataSources), 1) assert.Greater(t, len(Panels), 1) - assert.Equal(t, "app/plugins/datasource/graphite/module", DataSources["graphite"].Module) + assert.Equal(t, "app/plugins/datasource/graphite/module", pm.dataSources["graphite"].Module) assert.NotEmpty(t, Apps) assert.Equal(t, "public/plugins/test-app/img/logo_large.png", Apps["test-app"].Info.Logos.Large) assert.Equal(t, "public/plugins/test-app/img/screenshot2.png", Apps["test-app"].Info.Screenshots[1].Path) @@ -114,15 +114,15 @@ func TestPluginManager_Init(t *testing.T) { require.Empty(t, pm.scanningErrors) const pluginID = "test" - assert.NotNil(t, Plugins[pluginID]) - assert.Equal(t, "datasource", Plugins[pluginID].Type) - assert.Equal(t, "Test", Plugins[pluginID].Name) - assert.Equal(t, pluginID, Plugins[pluginID].Id) - assert.Equal(t, "1.0.0", Plugins[pluginID].Info.Version) - assert.Equal(t, plugins.PluginSignatureValid, Plugins[pluginID].Signature) - assert.Equal(t, plugins.GrafanaType, Plugins[pluginID].SignatureType) - assert.Equal(t, "Grafana Labs", Plugins[pluginID].SignatureOrg) - assert.False(t, Plugins[pluginID].IsCorePlugin) + assert.NotNil(t, pm.plugins[pluginID]) + assert.Equal(t, "datasource", pm.plugins[pluginID].Type) + assert.Equal(t, "Test", pm.plugins[pluginID].Name) + assert.Equal(t, pluginID, pm.plugins[pluginID].Id) + assert.Equal(t, "1.0.0", pm.plugins[pluginID].Info.Version) + assert.Equal(t, plugins.PluginSignatureValid, pm.plugins[pluginID].Signature) + assert.Equal(t, plugins.GrafanaType, pm.plugins[pluginID].SignatureType) + assert.Equal(t, "Grafana Labs", pm.plugins[pluginID].SignatureOrg) + assert.False(t, pm.plugins[pluginID].IsCorePlugin) }) t.Run("With back-end plugin with invalid v2 private signature (mismatched root URL)", func(t *testing.T) { @@ -139,7 +139,7 @@ func TestPluginManager_Init(t *testing.T) { require.NoError(t, err) assert.Equal(t, []error{fmt.Errorf(`plugin "test" has an invalid signature`)}, pm.scanningErrors) - assert.Nil(t, Plugins[("test")]) + assert.Nil(t, pm.plugins[("test")]) }) t.Run("With back-end plugin with valid v2 private signature", func(t *testing.T) { @@ -157,15 +157,15 @@ func TestPluginManager_Init(t *testing.T) { require.Empty(t, pm.scanningErrors) const pluginID = "test" - assert.NotNil(t, Plugins[pluginID]) - assert.Equal(t, "datasource", Plugins[pluginID].Type) - assert.Equal(t, "Test", Plugins[pluginID].Name) - assert.Equal(t, pluginID, Plugins[pluginID].Id) - assert.Equal(t, "1.0.0", Plugins[pluginID].Info.Version) - assert.Equal(t, plugins.PluginSignatureValid, Plugins[pluginID].Signature) - assert.Equal(t, plugins.PrivateType, Plugins[pluginID].SignatureType) - assert.Equal(t, "Will Browne", Plugins[pluginID].SignatureOrg) - assert.False(t, Plugins[pluginID].IsCorePlugin) + assert.NotNil(t, pm.plugins[pluginID]) + assert.Equal(t, "datasource", pm.plugins[pluginID].Type) + assert.Equal(t, "Test", pm.plugins[pluginID].Name) + assert.Equal(t, pluginID, pm.plugins[pluginID].Id) + assert.Equal(t, "1.0.0", pm.plugins[pluginID].Info.Version) + assert.Equal(t, plugins.PluginSignatureValid, pm.plugins[pluginID].Signature) + assert.Equal(t, plugins.PrivateType, pm.plugins[pluginID].SignatureType) + assert.Equal(t, "Will Browne", pm.plugins[pluginID].SignatureOrg) + assert.False(t, pm.plugins[pluginID].IsCorePlugin) }) t.Run("With back-end plugin with modified v2 signature (missing file from plugin dir)", func(t *testing.T) { @@ -181,7 +181,7 @@ func TestPluginManager_Init(t *testing.T) { err := pm.Init() require.NoError(t, err) assert.Equal(t, []error{fmt.Errorf(`plugin "test"'s signature has been modified`)}, pm.scanningErrors) - assert.Nil(t, Plugins[("test")]) + assert.Nil(t, pm.plugins[("test")]) }) t.Run("With back-end plugin with modified v2 signature (unaccounted file in plugin dir)", func(t *testing.T) { @@ -197,7 +197,7 @@ func TestPluginManager_Init(t *testing.T) { err := pm.Init() require.NoError(t, err) assert.Equal(t, []error{fmt.Errorf(`plugin "test"'s signature has been modified`)}, pm.scanningErrors) - assert.Nil(t, Plugins[("test")]) + assert.Nil(t, pm.plugins[("test")]) }) } @@ -260,6 +260,7 @@ func createManager(t *testing.T, cbs ...func(*PluginManager)) *PluginManager { StaticRootPath: staticRootPath, }, BackendPluginManager: &fakeBackendPluginManager{}, + dataSources: map[string]*plugins.DataSourcePlugin{}, } for _, cb := range cbs { cb(pm) diff --git a/pkg/plugins/manager/queries.go b/pkg/plugins/manager/queries.go index 6d101ba0ce5..b4d70666273 100644 --- a/pkg/plugins/manager/queries.go +++ b/pkg/plugins/manager/queries.go @@ -1,23 +1,22 @@ package manager import ( - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" ) -func (pm *PluginManager) GetPluginSettings(orgId int64) (map[string]*models.PluginSettingInfoDTO, error) { - query := models.GetPluginSettingsQuery{OrgId: orgId} - if err := bus.Dispatch(&query); err != nil { +func (pm *PluginManager) GetPluginSettings(orgID int64) (map[string]*models.PluginSettingInfoDTO, error) { + pluginSettings, err := pm.SQLStore.GetPluginSettings(orgID) + if err != nil { return nil, err } pluginMap := make(map[string]*models.PluginSettingInfoDTO) - for _, plug := range query.Result { + for _, plug := range pluginSettings { pluginMap[plug.PluginId] = plug } - for _, pluginDef := range Plugins { + for _, pluginDef := range pm.plugins { // ignore entries that exists if _, ok := pluginMap[pluginDef.Id]; ok { continue @@ -26,7 +25,7 @@ func (pm *PluginManager) GetPluginSettings(orgId int64) (map[string]*models.Plug // default to enabled true opt := &models.PluginSettingInfoDTO{ PluginId: pluginDef.Id, - OrgId: orgId, + OrgId: orgID, Enabled: true, } @@ -72,7 +71,7 @@ func (pm *PluginManager) GetEnabledPlugins(orgID int64) (*plugins.EnabledPlugins } // add all plugins that are not part of an App. - for dsID, ds := range DataSources { + for dsID, ds := range pm.dataSources { if _, exists := pluginSettingMap[ds.Id]; exists { enabledPlugins.DataSources[dsID] = ds } diff --git a/pkg/plugins/manager/update_checker.go b/pkg/plugins/manager/update_checker.go index 589dff03c5c..0d5888475f9 100644 --- a/pkg/plugins/manager/update_checker.go +++ b/pkg/plugins/manager/update_checker.go @@ -16,19 +16,19 @@ var ( httpClient = http.Client{Timeout: 10 * time.Second} ) -type GrafanaNetPlugin struct { +type grafanaNetPlugin struct { Slug string `json:"slug"` Version string `json:"version"` } -type GithubLatest struct { +type gitHubLatest struct { Stable string `json:"stable"` Testing string `json:"testing"` } -func getAllExternalPluginSlugs() string { +func (pm *PluginManager) getAllExternalPluginSlugs() string { var result []string - for _, plug := range Plugins { + for _, plug := range pm.plugins { if plug.IsCorePlugin { continue } @@ -46,7 +46,7 @@ func (pm *PluginManager) checkForUpdates() { pm.log.Debug("Checking for updates") - pluginSlugs := getAllExternalPluginSlugs() + pluginSlugs := pm.getAllExternalPluginSlugs() resp, err := httpClient.Get("https://grafana.com/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion) if err != nil { log.Tracef("Failed to get plugins repo from grafana.com, %v", err.Error()) @@ -64,14 +64,14 @@ func (pm *PluginManager) checkForUpdates() { return } - gNetPlugins := []GrafanaNetPlugin{} + gNetPlugins := []grafanaNetPlugin{} err = json.Unmarshal(body, &gNetPlugins) if err != nil { log.Tracef("Failed to unmarshal plugin repo, reading response from grafana.com, %v", err.Error()) return } - for _, plug := range Plugins { + for _, plug := range pm.plugins { for _, gplug := range gNetPlugins { if gplug.Slug == plug.Id { plug.GrafanaNetVersion = gplug.Version @@ -104,24 +104,24 @@ func (pm *PluginManager) checkForUpdates() { return } - var githubLatest GithubLatest - err = json.Unmarshal(body, &githubLatest) + var latest gitHubLatest + err = json.Unmarshal(body, &latest) if err != nil { log.Tracef("Failed to unmarshal github.com latest, reading response from github.com: %v", err.Error()) return } if strings.Contains(setting.BuildVersion, "-") { - pm.GrafanaLatestVersion = githubLatest.Testing - pm.GrafanaHasUpdate = !strings.HasPrefix(setting.BuildVersion, githubLatest.Testing) + pm.grafanaLatestVersion = latest.Testing + pm.grafanaHasUpdate = !strings.HasPrefix(setting.BuildVersion, latest.Testing) } else { - pm.GrafanaLatestVersion = githubLatest.Stable - pm.GrafanaHasUpdate = githubLatest.Stable != setting.BuildVersion + pm.grafanaLatestVersion = latest.Stable + pm.grafanaHasUpdate = latest.Stable != setting.BuildVersion } currVersion, err1 := version.NewVersion(setting.BuildVersion) - latestVersion, err2 := version.NewVersion(pm.GrafanaLatestVersion) + latestVersion, err2 := version.NewVersion(pm.grafanaLatestVersion) if err1 == nil && err2 == nil { - pm.GrafanaHasUpdate = currVersion.LessThan(latestVersion) + pm.grafanaHasUpdate = currVersion.LessThan(latestVersion) } } diff --git a/pkg/plugins/plugindashboards/service.go b/pkg/plugins/plugindashboards/service.go index 53e3d0c4b8d..2d1f8a3c65d 100644 --- a/pkg/plugins/plugindashboards/service.go +++ b/pkg/plugins/plugindashboards/service.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/tsdb" ) @@ -22,6 +23,7 @@ func init() { type Service struct { DataService *tsdb.Service `inject:""` PluginManager *manager.PluginManager `inject:""` + SQLStore *sqlstore.SQLStore `inject:""` logger log.Logger } @@ -40,19 +42,19 @@ func (s *Service) Run(ctx context.Context) error { func (s *Service) updateAppDashboards() { s.logger.Debug("Looking for app dashboard updates") - query := models.GetPluginSettingsQuery{OrgId: 0} - if err := bus.Dispatch(&query); err != nil { + pluginSettings, err := s.SQLStore.GetPluginSettings(0) + if err != nil { s.logger.Error("Failed to get all plugin settings", "error", err) return } - for _, pluginSetting := range query.Result { + for _, pluginSetting := range pluginSettings { // ignore disabled plugins if !pluginSetting.Enabled { continue } - if pluginDef, exists := manager.Plugins[pluginSetting.PluginId]; exists { + if pluginDef := s.PluginManager.GetPlugin(pluginSetting.PluginId); pluginDef != nil { if pluginDef.Info.Version != pluginSetting.PluginVersion { s.syncPluginDashboards(pluginDef, pluginSetting.OrgId) } @@ -117,7 +119,7 @@ func (s *Service) handlePluginStateChanged(event *models.PluginStateChangedEvent s.logger.Info("Plugin state changed", "pluginId", event.PluginId, "enabled", event.Enabled) if event.Enabled { - s.syncPluginDashboards(manager.Plugins[event.PluginId], event.OrgId) + s.syncPluginDashboards(s.PluginManager.GetPlugin(event.PluginId), event.OrgId) } else { query := models.GetDashboardsByPluginIdQuery{PluginId: event.PluginId, OrgId: event.OrgId} if err := bus.Dispatch(&query); err != nil { diff --git a/pkg/services/alerting/commands.go b/pkg/services/alerting/commands.go deleted file mode 100644 index 887334ec729..00000000000 --- a/pkg/services/alerting/commands.go +++ /dev/null @@ -1,36 +0,0 @@ -package alerting - -import ( - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/models" -) - -func init() { - bus.AddHandler("alerting", updateDashboardAlerts) - bus.AddHandler("alerting", validateDashboardAlerts) -} - -func validateDashboardAlerts(cmd *models.ValidateDashboardAlertsCommand) error { - extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId, cmd.User) - - return extractor.ValidateAlerts() -} - -func updateDashboardAlerts(cmd *models.UpdateDashboardAlertsCommand) error { - saveAlerts := models.SaveAlertsCommand{ - OrgId: cmd.OrgId, - UserId: cmd.User.UserId, - DashboardId: cmd.Dashboard.Id, - } - - extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId, cmd.User) - - alerts, err := extractor.GetAlerts() - if err != nil { - return err - } - - saveAlerts.Alerts = alerts - - return bus.Dispatch(&saveAlerts) -} diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 86b360e4482..133273333bd 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -10,10 +10,10 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/tsdb" "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/ext" tlog "github.com/opentracing/opentracing-go/log" @@ -27,7 +27,7 @@ type AlertEngine struct { RenderService rendering.Service `inject:""` Bus bus.Bus `inject:""` RequestValidator models.PluginRequestValidator `inject:""` - DataService *tsdb.Service `inject:""` + DataService plugins.DataRequestHandler `inject:""` execQueue chan *Job ticker *Ticker diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index afa45f890b3..129187a0363 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -61,7 +61,7 @@ func findPanelQueryByRefID(panel *simplejson.Json, refID string) *simplejson.Jso func copyJSON(in json.Marshaler) (*simplejson.Json, error) { rawJSON, err := in.MarshalJSON() if err != nil { - return nil, err + return nil, fmt.Errorf("JSON marshaling failed: %w", err) } return simplejson.NewJson(rawJSON) @@ -242,6 +242,8 @@ func (e *DashAlertExtractor) extractAlerts(validateFunc func(alert *models.Alert // ValidateAlerts validates alerts in the dashboard json but does not require a valid dashboard id // in the first validation pass. func (e *DashAlertExtractor) ValidateAlerts() error { - _, err := e.extractAlerts(func(alert *models.Alert) bool { return alert.OrgId != 0 && alert.PanelId != 0 }, false) + _, err := e.extractAlerts(func(alert *models.Alert) bool { + return alert.OrgId != 0 && alert.PanelId != 0 + }, false) return err } diff --git a/pkg/services/dashboards/acl_service.go b/pkg/services/dashboards/acl_service.go index 9392b760d68..b344ab3775c 100644 --- a/pkg/services/dashboards/acl_service.go +++ b/pkg/services/dashboards/acl_service.go @@ -3,11 +3,10 @@ package dashboards import ( "time" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" ) -func MakeUserAdmin(bus bus.Bus, orgID int64, userID int64, dashboardID int64, setViewAndEditPermissions bool) error { +func (dr *dashboardServiceImpl) MakeUserAdmin(orgID int64, userID int64, dashboardID int64, setViewAndEditPermissions bool) error { rtEditor := models.ROLE_EDITOR rtViewer := models.ROLE_VIEWER @@ -43,12 +42,7 @@ func MakeUserAdmin(bus bus.Bus, orgID int64, userID int64, dashboardID int64, se ) } - aclCmd := &models.UpdateDashboardAclCommand{ - DashboardID: dashboardID, - Items: items, - } - - if err := bus.Dispatch(aclCmd); err != nil { + if err := dr.dashboardStore.UpdateDashboardACL(dashboardID, items); err != nil { return err } diff --git a/pkg/services/dashboards/dashboard_service.go b/pkg/services/dashboards/dashboard_service.go index 354136c6411..1ebf34bb245 100644 --- a/pkg/services/dashboards/dashboard_service.go +++ b/pkg/services/dashboards/dashboard_service.go @@ -1,10 +1,13 @@ package dashboards import ( + "fmt" "strings" "time" "github.com/grafana/grafana/pkg/components/gtime" + "github.com/grafana/grafana/pkg/dashboards" + "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/bus" @@ -20,6 +23,7 @@ type DashboardService interface { SaveDashboard(dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) DeleteDashboard(dashboardId int64, orgId int64) error + MakeUserAdmin(orgID int64, userID, dashboardID int64, setViewAndEditPermissions bool) error } // DashboardProvisioningService is a service for operating on provisioned dashboards. @@ -27,21 +31,22 @@ type DashboardProvisioningService interface { SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) SaveFolderForProvisionedDashboards(*SaveDashboardDTO) (*models.Dashboard, error) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) - GetProvisionedDashboardDataByDashboardID(DashboardId int64) (*models.DashboardProvisioning, error) - UnprovisionDashboard(dashboardId int64) error - DeleteProvisionedDashboard(dashboardId int64, orgId int64) error + GetProvisionedDashboardDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) + UnprovisionDashboard(dashboardID int64) error + DeleteProvisionedDashboard(dashboardID int64, orgID int64) error } // NewService is a factory for creating a new dashboard service. -var NewService = func() DashboardService { +var NewService = func(store dashboards.Store) DashboardService { return &dashboardServiceImpl{ - log: log.New("dashboard-service"), + dashboardStore: store, + log: log.New("dashboard-service"), } } // NewProvisioningService is a factory for creating a new dashboard provisioning service. -var NewProvisioningService = func() DashboardProvisioningService { - return NewService().(*dashboardServiceImpl) +var NewProvisioningService = func(store dashboards.Store) DashboardProvisioningService { + return NewService(store).(*dashboardServiceImpl) } type SaveDashboardDTO struct { @@ -54,35 +59,32 @@ type SaveDashboardDTO struct { } type dashboardServiceImpl struct { - orgId int64 - user *models.SignedInUser - log log.Logger + dashboardStore dashboards.Store + orgId int64 + user *models.SignedInUser + log log.Logger } 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 + return dr.dashboardStore.GetProvisionedDashboardData(name) } -func (dr *dashboardServiceImpl) GetProvisionedDashboardDataByDashboardID(dashboardId int64) (*models.DashboardProvisioning, error) { - cmd := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: dashboardId} - err := bus.Dispatch(cmd) - if err != nil { - return nil, err - } - - return cmd.Result, nil +// GetProvisionedData gets provisioned dashboard data. +// +// Stubbable by tests. +var GetProvisionedData = func(store dashboards.Store, dashboardID int64) (*models.DashboardProvisioning, error) { + return store.GetProvisionedDataByDashboardID(dashboardID) } -func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlerts bool, +func (dr *dashboardServiceImpl) GetProvisionedDashboardDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) { + return GetProvisionedData(dr.dashboardStore, dashboardID) +} + +func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) { dash := dto.Dashboard + dash.OrgId = dto.OrgId dash.Title = strings.TrimSpace(dash.Title) dash.Data.Set("title", dash.Title) dash.SetUid(strings.TrimSpace(dash.Uid)) @@ -109,29 +111,18 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, return nil, err } - if validateAlerts { - validateAlertsCmd := models.ValidateDashboardAlertsCommand{ - OrgId: dto.OrgId, - Dashboard: dash, - User: dto.User, - } - - if err := bus.Dispatch(&validateAlertsCmd); err != nil { + if shouldValidateAlerts { + if err := validateAlerts(dash, dto.User); err != nil { return nil, err } } - validateBeforeSaveCmd := models.ValidateDashboardBeforeSaveCommand{ - OrgId: dto.OrgId, - Dashboard: dash, - Overwrite: dto.Overwrite, - } - - if err := bus.Dispatch(&validateBeforeSaveCmd); err != nil { + isParentFolderChanged, err := dr.dashboardStore.ValidateDashboardBeforeSave(dash, dto.Overwrite) + if err != nil { return nil, err } - if validateBeforeSaveCmd.Result.IsParentFolderChanged { + if isParentFolderChanged { folderGuardian := guardian.New(dash.FolderId, dto.OrgId, dto.User) if canSave, err := folderGuardian.CanSave(); err != nil || !canSave { if err != nil { @@ -178,6 +169,11 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, return cmd, nil } +var validateAlerts = func(dash *models.Dashboard, user *models.SignedInUser) error { + extractor := alerting.NewDashAlertExtractor(dash, dash.OrgId, user) + return extractor.ValidateAlerts() +} + func validateDashboardRefreshInterval(dash *models.Dashboard) error { if setting.MinRefreshInterval == "" { return nil @@ -191,11 +187,11 @@ func validateDashboardRefreshInterval(dash *models.Dashboard) error { minRefreshInterval, err := gtime.ParseDuration(setting.MinRefreshInterval) if err != nil { - return err + return fmt.Errorf("parsing min refresh interval %q failed: %w", setting.MinRefreshInterval, err) } d, err := gtime.ParseDuration(refresh) if err != nil { - return err + return fmt.Errorf("parsing refresh duration %q failed: %w", refresh, err) } if d < minRefreshInterval { @@ -205,14 +201,17 @@ func validateDashboardRefreshInterval(dash *models.Dashboard) error { return nil } -func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand, dto *SaveDashboardDTO) error { - alertCmd := models.UpdateDashboardAlertsCommand{ - OrgId: dto.OrgId, - Dashboard: cmd.Result, - User: dto.User, +// UpdateAlerting updates alerting. +// +// Stubbable by tests. +var UpdateAlerting = func(store dashboards.Store, orgID int64, dashboard *models.Dashboard, user *models.SignedInUser) error { + extractor := alerting.NewDashAlertExtractor(dashboard, orgID, user) + alerts, err := extractor.GetAlerts() + if err != nil { + return err } - return bus.Dispatch(&alertCmd) + return store.SaveAlerts(dashboard.Id, alerts) } func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO, @@ -234,24 +233,18 @@ func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO, return nil, err } - saveCmd := &models.SaveProvisionedDashboardCommand{ - DashboardCmd: cmd, - DashboardProvisioning: provisioning, - } - // dashboard - err = bus.Dispatch(saveCmd) + dash, err := dr.dashboardStore.SaveProvisionedDashboard(*cmd, provisioning) if err != nil { return nil, err } // alerts - err = dr.updateAlerting(cmd, dto) - if err != nil { + if err := UpdateAlerting(dr.dashboardStore, dto.OrgId, dash, dto.User); err != nil { return nil, err } - return cmd.Result, nil + return dash, nil } func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDashboardDTO) (*models.Dashboard, error) { @@ -264,22 +257,23 @@ func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDash return nil, err } - err = bus.Dispatch(cmd) + dash, err := dr.dashboardStore.SaveDashboard(*cmd) if err != nil { return nil, err } - err = dr.updateAlerting(cmd, dto) - if err != nil { + if err := UpdateAlerting(dr.dashboardStore, dto.OrgId, dash, dto.User); err != nil { return nil, err } - return cmd.Result, nil + return dash, nil } func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error) { if err := validateDashboardRefreshInterval(dto.Dashboard); err != nil { - dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval", "dashboardUid", dto.Dashboard.Uid, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", setting.MinRefreshInterval) + dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval", + "dashboardUid", dto.Dashboard.Uid, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", + setting.MinRefreshInterval) dto.Dashboard.Data.Set("refresh", setting.MinRefreshInterval) } @@ -288,17 +282,16 @@ func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO, allowUiUpda return nil, err } - err = bus.Dispatch(cmd) + dash, err := dr.dashboardStore.SaveDashboard(*cmd) if err != nil { + return nil, fmt.Errorf("saving dashboard failed: %w", err) + } + + if err := UpdateAlerting(dr.dashboardStore, dto.OrgId, dash, dto.User); err != nil { return nil, err } - err = dr.updateAlerting(cmd, dto) - if err != nil { - return nil, err - } - - return cmd.Result, nil + return dash, nil } // DeleteDashboard removes dashboard from the DB. Errors out if the dashboard was provisioned. Should be used for @@ -327,9 +320,12 @@ func (dr *dashboardServiceImpl) deleteDashboard(dashboardId int64, orgId int64, return bus.Dispatch(cmd) } -func (dr *dashboardServiceImpl) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) { +func (dr *dashboardServiceImpl) ImportDashboard(dto *SaveDashboardDTO) ( + *models.Dashboard, error) { if err := validateDashboardRefreshInterval(dto.Dashboard); err != nil { - dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval", "dashboardUid", dto.Dashboard.Uid, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", setting.MinRefreshInterval) + dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval", + "dashboardUid", dto.Dashboard.Uid, "dashboardTitle", dto.Dashboard.Title, + "minRefreshInterval", setting.MinRefreshInterval) dto.Dashboard.Data.Set("refresh", setting.MinRefreshInterval) } @@ -359,6 +355,7 @@ type FakeDashboardService struct { SaveDashboardResult *models.Dashboard SaveDashboardError error SavedDashboards []*SaveDashboardDTO + ProvisionedDashData *models.DashboardProvisioning } func (s *FakeDashboardService) SaveDashboard(dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error) { @@ -385,8 +382,12 @@ func (s *FakeDashboardService) DeleteDashboard(dashboardId int64, orgId int64) e return nil } +func (s *FakeDashboardService) GetProvisionedDashboardDataByDashboardID(id int64) (*models.DashboardProvisioning, error) { + return s.ProvisionedDashData, nil +} + func MockDashboardService(mock *FakeDashboardService) { - NewService = func() DashboardService { + NewService = func(dashboards.Store) DashboardService { return mock } } diff --git a/pkg/services/dashboards/dashboard_service_integration_test.go b/pkg/services/dashboards/dashboard_service_integration_test.go new file mode 100644 index 00000000000..355d6a5a4eb --- /dev/null +++ b/pkg/services/dashboards/dashboard_service_integration_test.go @@ -0,0 +1,928 @@ +// +build integration + +package dashboards + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/dashboards" + "github.com/grafana/grafana/pkg/services/guardian" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/models" +) + +const testOrgID int64 = 1 + +func TestIntegratedDashboardService(t *testing.T) { + t.Run("Given saved folders and dashboards in organization A", func(t *testing.T) { + origUpdateAlerting := UpdateAlerting + t.Cleanup(func() { + UpdateAlerting = origUpdateAlerting + }) + UpdateAlerting = func(store dashboards.Store, orgID int64, dashboard *models.Dashboard, user *models.SignedInUser) error { + return nil + } + + // Basic validation tests + + permissionScenario(t, "When saving a dashboard with non-existing id", true, + func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": float64(123412321), + "title": "Expect error", + }), + } + + err := callSaveWithError(cmd, sc.sqlStore) + assert.Equal(t, models.ErrDashboardNotFound, err) + }) + + // Given other organization + + t.Run("Given organization B", func(t *testing.T) { + const otherOrgId int64 = 2 + + permissionScenario(t, "When creating a dashboard with same id as dashboard in organization A", + true, func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: otherOrgId, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": sc.savedDashInFolder.Id, + "title": "Expect error", + }), + Overwrite: false, + } + + err := callSaveWithError(cmd, sc.sqlStore) + assert.Equal(t, models.ErrDashboardNotFound, err) + }) + + permissionScenario(t, "When creating a dashboard with same uid as dashboard in organization A, it should create a new dashboard in org B", + true, func(t *testing.T, sc *permissionScenarioContext) { + const otherOrgId int64 = 2 + cmd := models.SaveDashboardCommand{ + OrgId: otherOrgId, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "uid": sc.savedDashInFolder.Uid, + "title": "Dash with existing uid in other org", + }), + Overwrite: false, + } + + res := callSaveWithResult(t, cmd, sc.sqlStore) + require.NotNil(t, res) + + dash, err := sc.sqlStore.GetDashboard(0, otherOrgId, sc.savedDashInFolder.Uid, "") + require.NoError(t, err) + + assert.NotEqual(t, sc.savedDashInFolder.Id, dash.Id) + assert.Equal(t, res.Id, dash.Id) + assert.Equal(t, otherOrgId, dash.OrgId) + assert.Equal(t, sc.savedDashInFolder.Uid, dash.Uid) + }) + }) + + t.Run("Given user has no permission to save", func(t *testing.T) { + const canSave = false + + permissionScenario(t, "When creating a new dashboard in the General folder", canSave, + func(t *testing.T, sc *permissionScenarioContext) { + sqlStore := sqlstore.InitTestDB(t) + cmd := models.SaveDashboardCommand{ + OrgId: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "title": "Dash", + }), + UserId: 10000, + Overwrite: true, + } + + err := callSaveWithError(cmd, sqlStore) + assert.Equal(t, models.ErrDashboardUpdateAccessDenied, err) + + assert.Equal(t, int64(0), sc.dashboardGuardianMock.DashId) + assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId) + }) + + permissionScenario(t, "When creating a new dashboard in other folder, it should create dashboard guardian for other folder with correct arguments and rsult in access denied error", + canSave, func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "title": "Dash", + }), + FolderId: sc.otherSavedFolder.Id, + UserId: 10000, + Overwrite: true, + } + + err := callSaveWithError(cmd, sc.sqlStore) + require.Equal(t, models.ErrDashboardUpdateAccessDenied, err) + + assert.Equal(t, sc.otherSavedFolder.Id, sc.dashboardGuardianMock.DashId) + assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId) + }) + + permissionScenario(t, "When creating a new dashboard by existing title in folder, it should create dashboard guardian for folder with correct arguments and result in access denied error", + canSave, func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "title": sc.savedDashInFolder.Title, + }), + FolderId: sc.savedFolder.Id, + UserId: 10000, + Overwrite: true, + } + + err := callSaveWithError(cmd, sc.sqlStore) + require.Equal(t, models.ErrDashboardUpdateAccessDenied, err) + + assert.Equal(t, sc.savedFolder.Id, sc.dashboardGuardianMock.DashId) + assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId) + }) + + permissionScenario(t, "When creating a new dashboard by existing UID in folder, it should create dashboard guardian for folder with correct arguments and result in access denied error", + canSave, func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "uid": sc.savedDashInFolder.Uid, + "title": "New dash", + }), + FolderId: sc.savedFolder.Id, + UserId: 10000, + Overwrite: true, + } + + err := callSaveWithError(cmd, sc.sqlStore) + require.Equal(t, models.ErrDashboardUpdateAccessDenied, err) + + assert.Equal(t, sc.savedFolder.Id, sc.dashboardGuardianMock.DashId) + assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId) + }) + + permissionScenario(t, "When updating a dashboard by existing id in the General folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", + canSave, func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": sc.savedDashInGeneralFolder.Id, + "title": "Dash", + }), + FolderId: sc.savedDashInGeneralFolder.FolderId, + UserId: 10000, + Overwrite: true, + } + + err := callSaveWithError(cmd, sc.sqlStore) + assert.Equal(t, models.ErrDashboardUpdateAccessDenied, err) + + assert.Equal(t, sc.savedDashInGeneralFolder.Id, sc.dashboardGuardianMock.DashId) + assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId) + }) + + permissionScenario(t, "When updating a dashboard by existing id in other folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", + canSave, func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": sc.savedDashInFolder.Id, + "title": "Dash", + }), + FolderId: sc.savedDashInFolder.FolderId, + UserId: 10000, + Overwrite: true, + } + + err := callSaveWithError(cmd, sc.sqlStore) + require.Equal(t, models.ErrDashboardUpdateAccessDenied, err) + + assert.Equal(t, sc.savedDashInFolder.Id, sc.dashboardGuardianMock.DashId) + assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId) + }) + + permissionScenario(t, "When moving a dashboard by existing ID to other folder from General folder, it should create dashboard guardian for other folder with correct arguments and result in access denied error", + canSave, func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": sc.savedDashInGeneralFolder.Id, + "title": "Dash", + }), + FolderId: sc.otherSavedFolder.Id, + UserId: 10000, + Overwrite: true, + } + + err := callSaveWithError(cmd, sc.sqlStore) + require.Equal(t, models.ErrDashboardUpdateAccessDenied, err) + + assert.Equal(t, sc.otherSavedFolder.Id, sc.dashboardGuardianMock.DashId) + assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId) + }) + + permissionScenario(t, "When moving a dashboard by existing id to the General folder from other folder, it should create dashboard guardian for General folder with correct arguments and result in access denied error", + canSave, func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": sc.savedDashInFolder.Id, + "title": "Dash", + }), + FolderId: 0, + UserId: 10000, + Overwrite: true, + } + + err := callSaveWithError(cmd, sc.sqlStore) + assert.Equal(t, models.ErrDashboardUpdateAccessDenied, err) + + assert.Equal(t, int64(0), sc.dashboardGuardianMock.DashId) + assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId) + }) + + permissionScenario(t, "When moving a dashboard by existing uid to other folder from General folder, it should create dashboard guardian for other folder with correct arguments and result in access denied error", + canSave, func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "uid": sc.savedDashInGeneralFolder.Uid, + "title": "Dash", + }), + FolderId: sc.otherSavedFolder.Id, + UserId: 10000, + Overwrite: true, + } + + err := callSaveWithError(cmd, sc.sqlStore) + require.Equal(t, models.ErrDashboardUpdateAccessDenied, err) + + assert.Equal(t, sc.otherSavedFolder.Id, sc.dashboardGuardianMock.DashId) + assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId) + }) + + permissionScenario(t, "When moving a dashboard by existing UID to the General folder from other folder, it should create dashboard guardian for General folder with correct arguments and result in access denied error", + canSave, func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "uid": sc.savedDashInFolder.Uid, + "title": "Dash", + }), + FolderId: 0, + UserId: 10000, + Overwrite: true, + } + + err := callSaveWithError(cmd, sc.sqlStore) + require.Equal(t, models.ErrDashboardUpdateAccessDenied, err) + + assert.Equal(t, int64(0), sc.dashboardGuardianMock.DashId) + assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId) + }) + }) + + t.Run("Given user has permission to save", func(t *testing.T) { + const canSave = true + + t.Run("and overwrite flag is set to false", func(t *testing.T) { + const shouldOverwrite = false + + permissionScenario(t, "When creating a dashboard in General folder with same name as dashboard in other folder", + canSave, func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": sc.savedDashInFolder.Title, + }), + FolderId: 0, + Overwrite: shouldOverwrite, + } + + res := callSaveWithResult(t, cmd, sc.sqlStore) + require.NotNil(t, res) + + dash, err := sc.sqlStore.GetDashboard(res.Id, cmd.OrgId, "", "") + require.NoError(t, err) + assert.Equal(t, res.Id, dash.Id) + assert.Equal(t, int64(0), dash.FolderId) + }) + + permissionScenario(t, "When creating a dashboard in other folder with same name as dashboard in General folder", + canSave, func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": sc.savedDashInGeneralFolder.Title, + }), + FolderId: sc.savedFolder.Id, + Overwrite: shouldOverwrite, + } + + res := callSaveWithResult(t, cmd, sc.sqlStore) + require.NotNil(t, res) + + assert.NotEqual(t, sc.savedDashInGeneralFolder.Id, res.Id) + + dash, err := sc.sqlStore.GetDashboard(res.Id, cmd.OrgId, "", "") + require.NoError(t, err) + assert.Equal(t, sc.savedFolder.Id, dash.FolderId) + }) + + permissionScenario(t, "When creating a folder with same name as dashboard in other folder", + canSave, func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": sc.savedDashInFolder.Title, + }), + IsFolder: true, + Overwrite: shouldOverwrite, + } + + res := callSaveWithResult(t, cmd, sc.sqlStore) + require.NotNil(t, res) + + assert.NotEqual(t, sc.savedDashInGeneralFolder.Id, res.Id) + assert.True(t, res.IsFolder) + + dash, err := sc.sqlStore.GetDashboard(res.Id, cmd.OrgId, "", "") + require.NoError(t, err) + assert.Equal(t, int64(0), dash.FolderId) + assert.True(t, dash.IsFolder) + }) + + permissionScenario(t, "When saving a dashboard without id and uid and unique title in folder", + canSave, func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "title": "Dash without id and uid", + }), + Overwrite: shouldOverwrite, + } + + res := callSaveWithResult(t, cmd, sc.sqlStore) + require.NotNil(t, res) + + assert.Greater(t, res.Id, int64(0)) + assert.NotEmpty(t, res.Uid) + dash, err := sc.sqlStore.GetDashboard(res.Id, cmd.OrgId, "", "") + require.NoError(t, err) + assert.Equal(t, res.Id, dash.Id) + assert.Equal(t, res.Uid, dash.Uid) + }) + + permissionScenario(t, "When saving a dashboard when dashboard id is zero ", canSave, + func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": 0, + "title": "Dash with zero id", + }), + Overwrite: shouldOverwrite, + } + + res := callSaveWithResult(t, cmd, sc.sqlStore) + require.NotNil(t, res) + + dash, err := sc.sqlStore.GetDashboard(res.Id, cmd.OrgId, "", "") + require.NoError(t, err) + assert.Equal(t, res.Id, dash.Id) + }) + + permissionScenario(t, "When saving a dashboard in non-existing folder", canSave, + func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "title": "Expect error", + }), + FolderId: 123412321, + Overwrite: shouldOverwrite, + } + + err := callSaveWithError(cmd, sc.sqlStore) + assert.Equal(t, models.ErrDashboardFolderNotFound, err) + }) + + permissionScenario(t, "When updating an existing dashboard by id without current version", canSave, + func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": sc.savedDashInGeneralFolder.Id, + "title": "test dash 23", + }), + FolderId: sc.savedFolder.Id, + Overwrite: shouldOverwrite, + } + + err := callSaveWithError(cmd, sc.sqlStore) + assert.Equal(t, models.ErrDashboardVersionMismatch, err) + }) + + permissionScenario(t, "When updating an existing dashboard by id with current version", canSave, + func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": sc.savedDashInGeneralFolder.Id, + "title": "Updated title", + "version": sc.savedDashInGeneralFolder.Version, + }), + FolderId: sc.savedFolder.Id, + Overwrite: shouldOverwrite, + } + + res := callSaveWithResult(t, cmd, sc.sqlStore) + require.NotNil(t, res) + + dash, err := sc.sqlStore.GetDashboard(sc.savedDashInGeneralFolder.Id, cmd.OrgId, "", "") + require.NoError(t, err) + assert.Equal(t, "Updated title", dash.Title) + assert.Equal(t, sc.savedFolder.Id, dash.FolderId) + assert.Greater(t, dash.Version, sc.savedDashInGeneralFolder.Version) + }) + + permissionScenario(t, "When updating an existing dashboard by uid without current version", canSave, + func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "uid": sc.savedDashInFolder.Uid, + "title": "test dash 23", + }), + FolderId: 0, + Overwrite: shouldOverwrite, + } + + err := callSaveWithError(cmd, sc.sqlStore) + assert.Equal(t, models.ErrDashboardVersionMismatch, err) + }) + + permissionScenario(t, "When updating an existing dashboard by uid with current version", canSave, + func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "uid": sc.savedDashInFolder.Uid, + "title": "Updated title", + "version": sc.savedDashInFolder.Version, + }), + FolderId: 0, + Overwrite: shouldOverwrite, + } + + res := callSaveWithResult(t, cmd, sc.sqlStore) + require.NotNil(t, res) + + dash, err := sc.sqlStore.GetDashboard(sc.savedDashInFolder.Id, cmd.OrgId, "", "") + require.NoError(t, err) + assert.Equal(t, "Updated title", dash.Title) + assert.Equal(t, int64(0), dash.FolderId) + assert.Greater(t, dash.Version, sc.savedDashInFolder.Version) + }) + + permissionScenario(t, "When creating a dashboard with same name as dashboard in other folder", + canSave, func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": sc.savedDashInFolder.Title, + }), + FolderId: sc.savedDashInFolder.FolderId, + Overwrite: shouldOverwrite, + } + + err := callSaveWithError(cmd, sc.sqlStore) + assert.Equal(t, models.ErrDashboardWithSameNameInFolderExists, err) + }) + + permissionScenario(t, "When creating a dashboard with same name as dashboard in General folder", + canSave, func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": sc.savedDashInGeneralFolder.Title, + }), + FolderId: sc.savedDashInGeneralFolder.FolderId, + Overwrite: shouldOverwrite, + } + + err := callSaveWithError(cmd, sc.sqlStore) + assert.Equal(t, models.ErrDashboardWithSameNameInFolderExists, err) + }) + + permissionScenario(t, "When creating a folder with same name as existing folder", canSave, + func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": sc.savedFolder.Title, + }), + IsFolder: true, + Overwrite: shouldOverwrite, + } + + err := callSaveWithError(cmd, sc.sqlStore) + assert.Equal(t, models.ErrDashboardWithSameNameInFolderExists, err) + }) + }) + + t.Run("and overwrite flag is set to true", func(t *testing.T) { + const shouldOverwrite = true + + permissionScenario(t, "When updating an existing dashboard by id without current version", canSave, + func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": sc.savedDashInGeneralFolder.Id, + "title": "Updated title", + }), + FolderId: sc.savedFolder.Id, + Overwrite: shouldOverwrite, + } + + res := callSaveWithResult(t, cmd, sc.sqlStore) + require.NotNil(t, res) + + dash, err := sc.sqlStore.GetDashboard(sc.savedDashInGeneralFolder.Id, cmd.OrgId, "", "") + require.NoError(t, err) + assert.Equal(t, "Updated title", dash.Title) + assert.Equal(t, sc.savedFolder.Id, dash.FolderId) + assert.Greater(t, dash.Version, sc.savedDashInGeneralFolder.Version) + }) + + permissionScenario(t, "When updating an existing dashboard by uid without current version", canSave, + func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "uid": sc.savedDashInFolder.Uid, + "title": "Updated title", + }), + FolderId: 0, + Overwrite: shouldOverwrite, + } + + res := callSaveWithResult(t, cmd, sc.sqlStore) + require.NotNil(t, res) + + dash, err := sc.sqlStore.GetDashboard(sc.savedDashInFolder.Id, cmd.OrgId, "", "") + require.NoError(t, err) + assert.Equal(t, "Updated title", dash.Title) + assert.Equal(t, int64(0), dash.FolderId) + assert.Greater(t, dash.Version, sc.savedDashInFolder.Version) + }) + + permissionScenario(t, "When updating uid for existing dashboard using id", canSave, + func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": sc.savedDashInFolder.Id, + "uid": "new-uid", + "title": sc.savedDashInFolder.Title, + }), + Overwrite: shouldOverwrite, + } + + res := callSaveWithResult(t, cmd, sc.sqlStore) + require.NotNil(t, res) + assert.Equal(t, sc.savedDashInFolder.Id, res.Id) + assert.Equal(t, "new-uid", res.Uid) + + dash, err := sc.sqlStore.GetDashboard(sc.savedDashInFolder.Id, cmd.OrgId, "", "") + require.NoError(t, err) + assert.Equal(t, "new-uid", dash.Uid) + assert.Greater(t, dash.Version, sc.savedDashInFolder.Version) + }) + + permissionScenario(t, "When updating uid to an existing uid for existing dashboard using id", canSave, + func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": sc.savedDashInFolder.Id, + "uid": sc.savedDashInGeneralFolder.Uid, + "title": sc.savedDashInFolder.Title, + }), + Overwrite: shouldOverwrite, + } + + err := callSaveWithError(cmd, sc.sqlStore) + assert.Equal(t, models.ErrDashboardWithSameUIDExists, err) + }) + + permissionScenario(t, "When creating a dashboard with same name as dashboard in other folder", canSave, + func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": sc.savedDashInFolder.Title, + }), + FolderId: sc.savedDashInFolder.FolderId, + Overwrite: shouldOverwrite, + } + + res := callSaveWithResult(t, cmd, sc.sqlStore) + require.NotNil(t, res) + assert.Equal(t, sc.savedDashInFolder.Id, res.Id) + assert.Equal(t, sc.savedDashInFolder.Uid, res.Uid) + + dash, err := sc.sqlStore.GetDashboard(res.Id, cmd.OrgId, "", "") + require.NoError(t, err) + assert.Equal(t, res.Id, dash.Id) + assert.Equal(t, res.Uid, dash.Uid) + }) + + permissionScenario(t, "When creating a dashboard with same name as dashboard in General folder", canSave, + func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": sc.savedDashInGeneralFolder.Title, + }), + FolderId: sc.savedDashInGeneralFolder.FolderId, + Overwrite: shouldOverwrite, + } + + res := callSaveWithResult(t, cmd, sc.sqlStore) + require.NotNil(t, res) + assert.Equal(t, sc.savedDashInGeneralFolder.Id, res.Id) + assert.Equal(t, sc.savedDashInGeneralFolder.Uid, res.Uid) + + dash, err := sc.sqlStore.GetDashboard(res.Id, cmd.OrgId, "", "") + require.NoError(t, err) + assert.Equal(t, res.Id, dash.Id) + assert.Equal(t, res.Uid, dash.Uid) + }) + + permissionScenario(t, "When updating existing folder to a dashboard using id", canSave, + func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": sc.savedFolder.Id, + "title": "new title", + }), + IsFolder: false, + Overwrite: shouldOverwrite, + } + + err := callSaveWithError(cmd, sc.sqlStore) + assert.Equal(t, models.ErrDashboardTypeMismatch, err) + }) + + permissionScenario(t, "When updating existing dashboard to a folder using id", canSave, + func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": sc.savedDashInFolder.Id, + "title": "new folder title", + }), + IsFolder: true, + Overwrite: shouldOverwrite, + } + + err := callSaveWithError(cmd, sc.sqlStore) + assert.Equal(t, models.ErrDashboardTypeMismatch, err) + }) + + permissionScenario(t, "When updating existing folder to a dashboard using uid", canSave, + func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "uid": sc.savedFolder.Uid, + "title": "new title", + }), + IsFolder: false, + Overwrite: shouldOverwrite, + } + + err := callSaveWithError(cmd, sc.sqlStore) + assert.Equal(t, models.ErrDashboardTypeMismatch, err) + }) + + permissionScenario(t, "When updating existing dashboard to a folder using uid", canSave, + func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "uid": sc.savedDashInFolder.Uid, + "title": "new folder title", + }), + IsFolder: true, + Overwrite: shouldOverwrite, + } + + err := callSaveWithError(cmd, sc.sqlStore) + assert.Equal(t, models.ErrDashboardTypeMismatch, err) + }) + + permissionScenario(t, "When updating existing folder to a dashboard using title", canSave, + func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "title": sc.savedFolder.Title, + }), + IsFolder: false, + Overwrite: shouldOverwrite, + } + + err := callSaveWithError(cmd, sc.sqlStore) + assert.Equal(t, models.ErrDashboardWithSameNameAsFolder, err) + }) + + permissionScenario(t, "When updating existing dashboard to a folder using title", canSave, + func(t *testing.T, sc *permissionScenarioContext) { + cmd := models.SaveDashboardCommand{ + OrgId: 1, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "title": sc.savedDashInGeneralFolder.Title, + }), + IsFolder: true, + Overwrite: shouldOverwrite, + } + + err := callSaveWithError(cmd, sc.sqlStore) + assert.Equal(t, models.ErrDashboardFolderWithSameNameAsDashboard, err) + }) + }) + }) + }) +} + +type permissionScenarioContext struct { + dashboardGuardianMock *guardian.FakeDashboardGuardian + sqlStore *sqlstore.SQLStore + savedFolder *models.Dashboard + savedDashInFolder *models.Dashboard + otherSavedFolder *models.Dashboard + savedDashInGeneralFolder *models.Dashboard +} + +type permissionScenarioFunc func(t *testing.T, sc *permissionScenarioContext) + +func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionScenarioFunc) { + t.Helper() + + mock := &guardian.FakeDashboardGuardian{ + CanSaveValue: canSave, + } + + t.Run(desc, func(t *testing.T) { + sqlStore := sqlstore.InitTestDB(t) + + savedFolder := saveTestFolder(t, "Saved folder", testOrgID, sqlStore) + savedDashInFolder := saveTestDashboard(t, "Saved dash in folder", testOrgID, savedFolder.Id, sqlStore) + saveTestDashboard(t, "Other saved dash in folder", testOrgID, savedFolder.Id, sqlStore) + savedDashInGeneralFolder := saveTestDashboard(t, "Saved dashboard in general folder", testOrgID, 0, sqlStore) + otherSavedFolder := saveTestFolder(t, "Other saved folder", testOrgID, sqlStore) + + require.Equal(t, "Saved folder", savedFolder.Title) + require.Equal(t, "saved-folder", savedFolder.Slug) + require.NotEqual(t, int64(0), savedFolder.Id) + require.True(t, savedFolder.IsFolder) + require.Equal(t, int64(0), savedFolder.FolderId) + require.NotEmpty(t, savedFolder.Uid) + + require.Equal(t, "Saved dash in folder", savedDashInFolder.Title) + require.Equal(t, "saved-dash-in-folder", savedDashInFolder.Slug) + require.NotEqual(t, int64(0), savedDashInFolder.Id) + require.False(t, savedDashInFolder.IsFolder) + require.Equal(t, savedFolder.Id, savedDashInFolder.FolderId) + require.NotEmpty(t, savedDashInFolder.Uid) + + origNewDashboardGuardian := guardian.New + t.Cleanup(func() { + guardian.New = origNewDashboardGuardian + }) + guardian.MockDashboardGuardian(mock) + + sc := &permissionScenarioContext{ + dashboardGuardianMock: mock, + sqlStore: sqlStore, + savedDashInFolder: savedDashInFolder, + otherSavedFolder: otherSavedFolder, + savedDashInGeneralFolder: savedDashInGeneralFolder, + savedFolder: savedFolder, + } + + fn(t, sc) + }) +} + +func callSaveWithResult(t *testing.T, cmd models.SaveDashboardCommand, sqlStore *sqlstore.SQLStore) *models.Dashboard { + t.Helper() + + dto := toSaveDashboardDto(cmd) + res, err := NewService(sqlStore).SaveDashboard(&dto, false) + require.NoError(t, err) + + return res +} + +func callSaveWithError(cmd models.SaveDashboardCommand, sqlStore *sqlstore.SQLStore) error { + dto := toSaveDashboardDto(cmd) + _, err := NewService(sqlStore).SaveDashboard(&dto, false) + return err +} + +func saveTestDashboard(t *testing.T, title string, orgID, folderID int64, sqlStore *sqlstore.SQLStore) *models.Dashboard { + t.Helper() + + cmd := models.SaveDashboardCommand{ + OrgId: orgID, + FolderId: folderID, + IsFolder: false, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": title, + }), + } + + dto := SaveDashboardDTO{ + OrgId: orgID, + Dashboard: cmd.GetDashboardModel(), + User: &models.SignedInUser{ + UserId: 1, + OrgRole: models.ROLE_ADMIN, + }, + } + + res, err := NewService(sqlStore).SaveDashboard(&dto, false) + require.NoError(t, err) + + return res +} + +func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore *sqlstore.SQLStore) *models.Dashboard { + t.Helper() + cmd := models.SaveDashboardCommand{ + OrgId: orgID, + FolderId: 0, + IsFolder: true, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": title, + }), + } + + dto := SaveDashboardDTO{ + OrgId: orgID, + Dashboard: cmd.GetDashboardModel(), + User: &models.SignedInUser{ + UserId: 1, + OrgRole: models.ROLE_ADMIN, + }, + } + + res, err := NewService(sqlStore).SaveDashboard(&dto, false) + require.NoError(t, err) + + return res +} + +func toSaveDashboardDto(cmd models.SaveDashboardCommand) SaveDashboardDTO { + dash := (&cmd).GetDashboardModel() + + return SaveDashboardDTO{ + Dashboard: dash, + Message: cmd.Message, + OrgId: cmd.OrgId, + User: &models.SignedInUser{UserId: cmd.UserId}, + Overwrite: cmd.Overwrite, + } +} diff --git a/pkg/services/dashboards/dashboard_service_test.go b/pkg/services/dashboards/dashboard_service_test.go index ab569629a31..033d3d32b3c 100644 --- a/pkg/services/dashboards/dashboard_service_test.go +++ b/pkg/services/dashboards/dashboard_service_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/grafana/grafana/pkg/dashboards" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/setting" @@ -17,8 +18,10 @@ func TestDashboardService(t *testing.T) { Convey("Dashboard service tests", t, func() { bus.ClearBusHandlers() + fakeStore := fakeDashboardStore{} service := &dashboardServiceImpl{ - log: log.New("test.logger"), + log: log.New("test.logger"), + dashboardStore: &fakeStore, } origNewDashboardGuardian := guardian.New @@ -51,19 +54,13 @@ func TestDashboardService(t *testing.T) { }) Convey("When saving a dashboard should validate uid", func() { - bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error { - return nil + origValidateAlerts := validateAlerts + t.Cleanup(func() { + validateAlerts = origValidateAlerts }) - - bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error { - cmd.Result = &models.ValidateDashboardBeforeSaveResult{} + validateAlerts = func(dash *models.Dashboard, user *models.SignedInUser) error { return nil - }) - - bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error { - cmd.Result = nil - return nil - }) + } testCases := []struct { Uid string @@ -89,27 +86,23 @@ func TestDashboardService(t *testing.T) { }) Convey("Should return validation error if dashboard is provisioned", func() { - provisioningValidated := false - bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error { - provisioningValidated = true - cmd.Result = &models.DashboardProvisioning{} - return nil + t.Cleanup(func() { + fakeStore.provisionedData = nil }) + fakeStore.provisionedData = &models.DashboardProvisioning{} - bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error { - return nil + origValidateAlerts := validateAlerts + t.Cleanup(func() { + validateAlerts = origValidateAlerts }) - - bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error { - cmd.Result = &models.ValidateDashboardBeforeSaveResult{} + validateAlerts = func(dash *models.Dashboard, user *models.SignedInUser) error { return nil - }) + } dto.Dashboard = models.NewDashboard("Dash") dto.Dashboard.SetId(3) dto.User = &models.SignedInUser{UserId: 1} _, err := service.SaveDashboard(dto, false) - So(provisioningValidated, ShouldBeTrue) So(err, ShouldEqual, models.ErrDashboardCannotSaveProvisionedDashboard) }) @@ -121,21 +114,20 @@ func TestDashboardService(t *testing.T) { return nil }) - bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error { - return nil + origValidateAlerts := validateAlerts + t.Cleanup(func() { + validateAlerts = origValidateAlerts }) - - bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error { - cmd.Result = &models.ValidateDashboardBeforeSaveResult{} + validateAlerts = func(dash *models.Dashboard, user *models.SignedInUser) error { return nil - }) + } dto.Dashboard = models.NewDashboard("Dash") dto.Dashboard.SetId(3) dto.User = &models.SignedInUser{UserId: 1} _, err := service.SaveDashboard(dto, true) So(provisioningValidated, ShouldBeFalse) - So(err, ShouldNotBeNil) + So(err, ShouldBeNil) }) Convey("Should return validation error if alert data is invalid", func() { @@ -144,9 +136,13 @@ func TestDashboardService(t *testing.T) { return nil }) - bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error { - return fmt.Errorf("alert validation error") + origValidateAlerts := validateAlerts + t.Cleanup(func() { + validateAlerts = origValidateAlerts }) + validateAlerts = func(dash *models.Dashboard, user *models.SignedInUser) error { + return fmt.Errorf("alert validation error") + } dto.Dashboard = models.NewDashboard("Dash") _, err := service.SaveDashboard(dto, false) @@ -165,23 +161,27 @@ func TestDashboardService(t *testing.T) { return nil }) - bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error { - return nil + origUpdateAlerting := UpdateAlerting + t.Cleanup(func() { + UpdateAlerting = origUpdateAlerting }) + UpdateAlerting = func(store dashboards.Store, orgID int64, dashboard *models.Dashboard, + user *models.SignedInUser) error { + return nil + } - bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error { - cmd.Result = &models.ValidateDashboardBeforeSaveResult{} - return nil + origValidateAlerts := validateAlerts + t.Cleanup(func() { + validateAlerts = origValidateAlerts }) + validateAlerts = func(dash *models.Dashboard, user *models.SignedInUser) error { + return nil + } bus.AddHandler("test", func(cmd *models.SaveProvisionedDashboardCommand) error { return nil }) - bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error { - return nil - }) - dto.Dashboard = models.NewDashboard("Dash") dto.Dashboard.SetId(3) dto.User = &models.SignedInUser{UserId: 1} @@ -200,22 +200,26 @@ func TestDashboardService(t *testing.T) { return nil }) - bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error { - return nil + origValidateAlerts := validateAlerts + t.Cleanup(func() { + validateAlerts = origValidateAlerts }) - - bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error { - cmd.Result = &models.ValidateDashboardBeforeSaveResult{} + validateAlerts = func(dash *models.Dashboard, user *models.SignedInUser) error { return nil - }) + } bus.AddHandler("test", func(cmd *models.SaveProvisionedDashboardCommand) error { return nil }) - bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error { - return nil + origUpdateAlerting := UpdateAlerting + t.Cleanup(func() { + UpdateAlerting = origUpdateAlerting }) + UpdateAlerting = func(store dashboards.Store, orgID int64, dashboard *models.Dashboard, + user *models.SignedInUser) error { + return nil + } dto.Dashboard = models.NewDashboard("Dash") dto.Dashboard.SetId(3) @@ -231,41 +235,42 @@ func TestDashboardService(t *testing.T) { dto := &SaveDashboardDTO{} Convey("Should return validation error if dashboard is provisioned", func() { - provisioningValidated := false - bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error { - provisioningValidated = true - cmd.Result = &models.DashboardProvisioning{} - return nil + t.Cleanup(func() { + fakeStore.provisionedData = nil }) + fakeStore.provisionedData = &models.DashboardProvisioning{} - bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error { - return nil + origValidateAlerts := validateAlerts + t.Cleanup(func() { + validateAlerts = origValidateAlerts }) - - bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error { - cmd.Result = &models.ValidateDashboardBeforeSaveResult{} + validateAlerts = func(dash *models.Dashboard, user *models.SignedInUser) error { return nil - }) + } bus.AddHandler("test", func(cmd *models.SaveProvisionedDashboardCommand) error { return nil }) - bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error { - return nil + origUpdateAlerting := UpdateAlerting + t.Cleanup(func() { + UpdateAlerting = origUpdateAlerting }) + UpdateAlerting = func(store dashboards.Store, orgID int64, dashboard *models.Dashboard, + user *models.SignedInUser) error { + return nil + } dto.Dashboard = models.NewDashboard("Dash") dto.Dashboard.SetId(3) dto.User = &models.SignedInUser{UserId: 1} _, err := service.ImportDashboard(dto) - So(provisioningValidated, ShouldBeTrue) So(err, ShouldEqual, models.ErrDashboardCannotSaveProvisionedDashboard) }) }) Convey("Given provisioned dashboard", func() { - result := setupDeleteHandlers(true) + result := setupDeleteHandlers(t, &fakeStore, true) Convey("DeleteProvisionedDashboard should delete it", func() { err := service.DeleteProvisionedDashboard(1, 1) @@ -281,7 +286,7 @@ func TestDashboardService(t *testing.T) { }) Convey("Given non provisioned dashboard", func() { - result := setupDeleteHandlers(false) + result := setupDeleteHandlers(t, &fakeStore, false) Convey("DeleteProvisionedDashboard should delete it", func() { err := service.DeleteProvisionedDashboard(1, 1) @@ -306,15 +311,15 @@ type Result struct { deleteWasCalled bool } -func setupDeleteHandlers(provisioned bool) *Result { - bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error { - if provisioned { - cmd.Result = &models.DashboardProvisioning{} - } else { - cmd.Result = nil - } - return nil +func setupDeleteHandlers(t *testing.T, fakeStore *fakeDashboardStore, provisioned bool) *Result { + t.Helper() + + t.Cleanup(func() { + fakeStore.provisionedData = nil }) + if provisioned { + fakeStore.provisionedData = &models.DashboardProvisioning{} + } result := &Result{} bus.AddHandler("test", func(cmd *models.DeleteDashboardCommand) error { @@ -326,3 +331,32 @@ func setupDeleteHandlers(provisioned bool) *Result { return result } + +type fakeDashboardStore struct { + dashboards.Store + + validationError error + provisionedData *models.DashboardProvisioning +} + +func (s *fakeDashboardStore) ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) ( + bool, error) { + return false, s.validationError +} + +func (s *fakeDashboardStore) GetProvisionedDataByDashboardID(int64) (*models.DashboardProvisioning, error) { + return s.provisionedData, nil +} + +func (s *fakeDashboardStore) SaveProvisionedDashboard(models.SaveDashboardCommand, + *models.DashboardProvisioning) (*models.Dashboard, error) { + return nil, nil +} + +func (s *fakeDashboardStore) SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error) { + return cmd.GetDashboardModel(), nil +} + +func (s *fakeDashboardStore) SaveAlerts(dashID int64, alerts []*models.Alert) error { + return nil +} diff --git a/pkg/services/dashboards/folder_service.go b/pkg/services/dashboards/folder_service.go index df7fb229601..0810c149e98 100644 --- a/pkg/services/dashboards/folder_service.go +++ b/pkg/services/dashboards/folder_service.go @@ -2,28 +2,32 @@ package dashboards import ( "errors" + "strings" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/dashboards" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/search" ) -// FolderService service for operating on folders +// FolderService is a service for operating on folders. type FolderService interface { GetFolders(limit int64) ([]*models.Folder, error) GetFolderByID(id int64) (*models.Folder, error) GetFolderByUID(uid string) (*models.Folder, error) - CreateFolder(cmd *models.CreateFolderCommand) error + CreateFolder(title, uid string) (*models.Folder, error) UpdateFolder(uid string, cmd *models.UpdateFolderCommand) error DeleteFolder(uid string) (*models.Folder, error) + MakeUserAdmin(orgID int64, userID, folderID int64, setViewAndEditPermissions bool) error } -// NewFolderService factory for creating a new folder service -var NewFolderService = func(orgId int64, user *models.SignedInUser) FolderService { +// NewFolderService is a factory for creating a new folder service. +var NewFolderService = func(orgID int64, user *models.SignedInUser, store dashboards.Store) FolderService { return &dashboardServiceImpl{ - orgId: orgId, - user: user, + orgId: orgID, + user: user, + dashboardStore: store, } } @@ -58,7 +62,6 @@ func (dr *dashboardServiceImpl) GetFolders(limit int64) ([]*models.Folder, error func (dr *dashboardServiceImpl) GetFolderByID(id int64) (*models.Folder, error) { query := models.GetDashboardQuery{OrgId: dr.orgId, Id: id} dashFolder, err := getFolder(query) - if err != nil { return nil, toFolderError(err) } @@ -93,8 +96,17 @@ func (dr *dashboardServiceImpl) GetFolderByUID(uid string) (*models.Folder, erro return dashToFolder(dashFolder), nil } -func (dr *dashboardServiceImpl) CreateFolder(cmd *models.CreateFolderCommand) error { - dashFolder := cmd.GetDashboardModel(dr.orgId, dr.user.UserId) +func (dr *dashboardServiceImpl) CreateFolder(title, uid string) (*models.Folder, error) { + dashFolder := models.NewDashboardFolder(title) + dashFolder.OrgId = dr.orgId + dashFolder.SetUid(strings.TrimSpace(uid)) + userID := dr.user.UserId + if userID == 0 { + userID = -1 + } + dashFolder.CreatedBy = userID + dashFolder.UpdatedBy = userID + dashFolder.UpdateSlug() dto := &SaveDashboardDTO{ Dashboard: dashFolder, @@ -104,23 +116,21 @@ func (dr *dashboardServiceImpl) CreateFolder(cmd *models.CreateFolderCommand) er saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false, false) if err != nil { - return toFolderError(err) + return nil, toFolderError(err) } - err = bus.Dispatch(saveDashboardCmd) + dash, err := dr.dashboardStore.SaveDashboard(*saveDashboardCmd) if err != nil { - return toFolderError(err) + return nil, toFolderError(err) } - query := models.GetDashboardQuery{OrgId: dr.orgId, Id: saveDashboardCmd.Result.Id} + query := models.GetDashboardQuery{OrgId: dr.orgId, Id: dash.Id} dashFolder, err = getFolder(query) if err != nil { - return toFolderError(err) + return nil, toFolderError(err) } - cmd.Result = dashToFolder(dashFolder) - - return nil + return dashToFolder(dashFolder), nil } func (dr *dashboardServiceImpl) UpdateFolder(existingUid string, cmd *models.UpdateFolderCommand) error { @@ -144,12 +154,12 @@ func (dr *dashboardServiceImpl) UpdateFolder(existingUid string, cmd *models.Upd return toFolderError(err) } - err = bus.Dispatch(saveDashboardCmd) + dash, err := dr.dashboardStore.SaveDashboard(*saveDashboardCmd) if err != nil { return toFolderError(err) } - query = models.GetDashboardQuery{OrgId: dr.orgId, Id: saveDashboardCmd.Result.Id} + query = models.GetDashboardQuery{OrgId: dr.orgId, Id: dash.Id} dashFolder, err = getFolder(query) if err != nil { return toFolderError(err) diff --git a/pkg/services/dashboards/folder_service_test.go b/pkg/services/dashboards/folder_service_test.go index 9631bc90646..6eb414a352f 100644 --- a/pkg/services/dashboards/folder_service_test.go +++ b/pkg/services/dashboards/folder_service_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/dashboards" "github.com/grafana/grafana/pkg/models" "github.com/stretchr/testify/assert" @@ -15,8 +16,9 @@ import ( func TestFolderService(t *testing.T) { Convey("Folder service tests", t, func() { service := dashboardServiceImpl{ - orgId: 1, - user: &models.SignedInUser{UserId: 1}, + orgId: 1, + user: &models.SignedInUser{UserId: 1}, + dashboardStore: &fakeDashboardStore{}, } Convey("Given user has no permissions", func() { @@ -28,32 +30,26 @@ func TestFolderService(t *testing.T) { return nil }) - bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error { - return nil - }) - - bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error { - cmd.Result = &models.ValidateDashboardBeforeSaveResult{} - return models.ErrDashboardUpdateAccessDenied + origStore := service.dashboardStore + t.Cleanup(func() { + service.dashboardStore = origStore }) + service.dashboardStore = &fakeDashboardStore{ + validationError: models.ErrDashboardUpdateAccessDenied, + } Convey("When get folder by id should return access denied error", func() { _, err := service.GetFolderByID(1) - So(err, ShouldNotBeNil) So(err, ShouldEqual, models.ErrFolderAccessDenied) }) Convey("When get folder by uid should return access denied error", func() { _, err := service.GetFolderByUID("uid") - So(err, ShouldNotBeNil) So(err, ShouldEqual, models.ErrFolderAccessDenied) }) Convey("When creating folder should return access denied error", func() { - err := service.CreateFolder(&models.CreateFolderCommand{ - Title: "Folder", - }) - So(err, ShouldNotBeNil) + _, err := service.CreateFolder("Folder", "") So(err, ShouldEqual, models.ErrFolderAccessDenied) }) @@ -62,7 +58,6 @@ func TestFolderService(t *testing.T) { Uid: "uid", Title: "Folder", }) - So(err, ShouldNotBeNil) So(err, ShouldEqual, models.ErrFolderAccessDenied) }) @@ -89,18 +84,14 @@ func TestFolderService(t *testing.T) { return nil }) - bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error { - return nil + origUpdateAlerting := UpdateAlerting + t.Cleanup(func() { + UpdateAlerting = origUpdateAlerting }) - - bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error { - cmd.Result = &models.ValidateDashboardBeforeSaveResult{} + UpdateAlerting = func(store dashboards.Store, orgID int64, dashboard *models.Dashboard, + user *models.SignedInUser) error { return nil - }) - - bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error { - return nil - }) + } bus.AddHandler("test", func(cmd *models.SaveDashboardCommand) error { cmd.Result = dash @@ -120,9 +111,7 @@ func TestFolderService(t *testing.T) { }) Convey("When creating folder should not return access denied error", func() { - err := service.CreateFolder(&models.CreateFolderCommand{ - Title: "Folder", - }) + _, err := service.CreateFolder("Folder", "") So(err, ShouldBeNil) So(provisioningValidated, ShouldBeFalse) }) diff --git a/pkg/services/librarypanels/database.go b/pkg/services/librarypanels/database.go index 783c0d4b6c9..7f0d6584e05 100644 --- a/pkg/services/librarypanels/database.go +++ b/pkg/services/librarypanels/database.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/util" ) @@ -67,7 +66,7 @@ func (lps *LibraryPanelService) createLibraryPanel(c *models.ReqContext, cmd cre } err := lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { - if err := requirePermissionsOnFolder(c.SignedInUser, cmd.FolderID); err != nil { + if err := lps.requirePermissionsOnFolder(c.SignedInUser, cmd.FolderID); err != nil { return err } if _, err := session.Insert(&libraryPanel); err != nil { @@ -108,12 +107,22 @@ func (lps *LibraryPanelService) createLibraryPanel(c *models.ReqContext, cmd cre return dto, err } -func connectDashboard(session *sqlstore.DBSession, dialect migrator.Dialect, user *models.SignedInUser, uid string, dashboardID int64) error { +// connectDashboard adds a connection between a Library Panel and a Dashboard. +func (lps *LibraryPanelService) connectDashboard(c *models.ReqContext, uid string, dashboardID int64) error { + err := lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { + return lps.implConnectDashboard(session, c.SignedInUser, uid, dashboardID) + }) + + return err +} + +func (lps *LibraryPanelService) implConnectDashboard(session *sqlstore.DBSession, user *models.SignedInUser, + uid string, dashboardID int64) error { panel, err := getLibraryPanel(session, uid, user.OrgId) if err != nil { return err } - if err := requirePermissionsOnFolder(user, panel.FolderID); err != nil { + if err := lps.requirePermissionsOnFolder(user, panel.FolderID); err != nil { return err } @@ -126,7 +135,7 @@ func connectDashboard(session *sqlstore.DBSession, dialect migrator.Dialect, use CreatedBy: user.UserId, } if _, err := session.Insert(&libraryPanelDashboard); err != nil { - if dialect.IsUniqueConstraintViolation(err) { + if lps.SQLStore.Dialect.IsUniqueConstraintViolation(err) { return nil } return err @@ -134,15 +143,6 @@ func connectDashboard(session *sqlstore.DBSession, dialect migrator.Dialect, use return nil } -// connectDashboard adds a connection between a Library Panel and a Dashboard. -func (lps *LibraryPanelService) connectDashboard(c *models.ReqContext, uid string, dashboardID int64) error { - err := lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { - return connectDashboard(session, lps.SQLStore.Dialect, c.SignedInUser, uid, dashboardID) - }) - - return err -} - // connectLibraryPanelsForDashboard adds connections for all Library Panels in a Dashboard. func (lps *LibraryPanelService) connectLibraryPanelsForDashboard(c *models.ReqContext, uids []string, dashboardID int64) error { err := lps.SQLStore.WithTransactionalDbSession(c.Context.Req.Context(), func(session *sqlstore.DBSession) error { @@ -151,7 +151,7 @@ func (lps *LibraryPanelService) connectLibraryPanelsForDashboard(c *models.ReqCo return err } for _, uid := range uids { - err := connectDashboard(session, lps.SQLStore.Dialect, c.SignedInUser, uid, dashboardID) + err := lps.implConnectDashboard(session, c.SignedInUser, uid, dashboardID) if err != nil { return err } @@ -169,7 +169,7 @@ func (lps *LibraryPanelService) deleteLibraryPanel(c *models.ReqContext, uid str if err != nil { return err } - if err := requirePermissionsOnFolder(c.SignedInUser, panel.FolderID); err != nil { + if err := lps.requirePermissionsOnFolder(c.SignedInUser, panel.FolderID); err != nil { return err } if _, err := session.Exec("DELETE FROM library_panel_dashboard WHERE librarypanel_id=?", panel.ID); err != nil { @@ -197,7 +197,7 @@ func (lps *LibraryPanelService) disconnectDashboard(c *models.ReqContext, uid st if err != nil { return err } - if err := requirePermissionsOnFolder(c.SignedInUser, panel.FolderID); err != nil { + if err := lps.requirePermissionsOnFolder(c.SignedInUser, panel.FolderID); err != nil { return err } @@ -248,7 +248,7 @@ func (lps *LibraryPanelService) deleteLibraryPanelsInFolder(c *models.ReqContext } folderID := folderUIDs[0].ID - if err := requirePermissionsOnFolder(c.SignedInUser, folderID); err != nil { + if err := lps.requirePermissionsOnFolder(c.SignedInUser, folderID); err != nil { return err } var dashIDs []struct { @@ -496,7 +496,8 @@ func (lps *LibraryPanelService) getLibraryPanelsForDashboardID(c *models.ReqCont return libraryPanelMap, err } -func handleFolderIDPatches(panelToPatch *LibraryPanel, fromFolderID int64, toFolderID int64, user *models.SignedInUser) error { +func (lps *LibraryPanelService) handleFolderIDPatches(panelToPatch *LibraryPanel, fromFolderID int64, + toFolderID int64, user *models.SignedInUser) error { // FolderID was not provided in the PATCH request if toFolderID == -1 { toFolderID = fromFolderID @@ -504,13 +505,13 @@ func handleFolderIDPatches(panelToPatch *LibraryPanel, fromFolderID int64, toFol // FolderID was provided in the PATCH request if toFolderID != -1 && toFolderID != fromFolderID { - if err := requirePermissionsOnFolder(user, toFolderID); err != nil { + if err := lps.requirePermissionsOnFolder(user, toFolderID); err != nil { return err } } // Always check permissions for the folder where library panel resides - if err := requirePermissionsOnFolder(user, fromFolderID); err != nil { + if err := lps.requirePermissionsOnFolder(user, fromFolderID); err != nil { return err } @@ -551,7 +552,7 @@ func (lps *LibraryPanelService) patchLibraryPanel(c *models.ReqContext, cmd patc if cmd.Model == nil { libraryPanel.Model = panelInDB.Model } - if err := handleFolderIDPatches(&libraryPanel, panelInDB.FolderID, cmd.FolderID, c.SignedInUser); err != nil { + if err := lps.handleFolderIDPatches(&libraryPanel, panelInDB.FolderID, cmd.FolderID, c.SignedInUser); err != nil { return err } if err := syncTitleWithName(&libraryPanel); err != nil { diff --git a/pkg/services/librarypanels/guard.go b/pkg/services/librarypanels/guard.go index 10c2cc1a2a0..0392a95ef9d 100644 --- a/pkg/services/librarypanels/guard.go +++ b/pkg/services/librarypanels/guard.go @@ -10,7 +10,7 @@ func isGeneralFolder(folderID int64) bool { return folderID == 0 } -func requirePermissionsOnFolder(user *models.SignedInUser, folderID int64) error { +func (lps *LibraryPanelService) requirePermissionsOnFolder(user *models.SignedInUser, folderID int64) error { if isGeneralFolder(folderID) && user.HasRole(models.ROLE_EDITOR) { return nil } @@ -19,7 +19,7 @@ func requirePermissionsOnFolder(user *models.SignedInUser, folderID int64) error return models.ErrFolderAccessDenied } - s := dashboards.NewFolderService(user.OrgId, user) + s := dashboards.NewFolderService(user.OrgId, user, lps.SQLStore) folder, err := s.GetFolderByID(folderID) if err != nil { return err diff --git a/pkg/services/librarypanels/librarypanels_connections_test.go b/pkg/services/librarypanels/librarypanels_connections_test.go index 816e6305f26..4ec3a1d71c0 100644 --- a/pkg/services/librarypanels/librarypanels_connections_test.go +++ b/pkg/services/librarypanels/librarypanels_connections_test.go @@ -74,11 +74,11 @@ func TestGetConnectedDashboards(t *testing.T) { scenarioWithLibraryPanel(t, "When an admin tries to get connected dashboards for a library panel that exists and has connections, it should return connected dashboard IDs", func(t *testing.T, sc scenarioContext) { - firstDash := createDashboard(t, sc.user, "Dash 1", 0) + firstDash := createDashboard(t, sc.sqlStore, sc.user, "Dash 1", 0) sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": strconv.FormatInt(firstDash.Id, 10)}) resp := sc.service.connectHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - secondDash := createDashboard(t, sc.user, "Dash 2", 0) + secondDash := createDashboard(t, sc.sqlStore, sc.user, "Dash 2", 0) sc.reqContext.ReplaceAllParams(map[string]string{":uid": sc.initialResult.Result.UID, ":dashboardId": strconv.FormatInt(secondDash.Id, 10)}) resp = sc.service.connectHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) diff --git a/pkg/services/librarypanels/librarypanels_patch_test.go b/pkg/services/librarypanels/librarypanels_patch_test.go index cd0627708ef..9f88b655ea2 100644 --- a/pkg/services/librarypanels/librarypanels_patch_test.go +++ b/pkg/services/librarypanels/librarypanels_patch_test.go @@ -25,7 +25,7 @@ func TestPatchLibraryPanel(t *testing.T) { resp = sc.service.connectHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) - newFolder := createFolderWithACL(t, "NewFolder", sc.user, []folderACLItem{}) + newFolder := createFolderWithACL(t, sc.sqlStore, "NewFolder", sc.user, []folderACLItem{}) cmd := patchLibraryPanelCommand{ FolderID: newFolder.Id, Name: "Panel - New name", @@ -82,7 +82,7 @@ func TestPatchLibraryPanel(t *testing.T) { scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel with folder only, it should change folder successfully and return correct result", func(t *testing.T, sc scenarioContext) { - newFolder := createFolderWithACL(t, "NewFolder", sc.user, []folderACLItem{}) + newFolder := createFolderWithACL(t, sc.sqlStore, "NewFolder", sc.user, []folderACLItem{}) cmd := patchLibraryPanelCommand{ FolderID: newFolder.Id, Version: 1, @@ -174,7 +174,7 @@ func TestPatchLibraryPanel(t *testing.T) { scenarioWithLibraryPanel(t, "When an admin tries to patch a library panel with a folder where a library panel with the same name already exists, it should fail", func(t *testing.T, sc scenarioContext) { - newFolder := createFolderWithACL(t, "NewFolder", sc.user, []folderACLItem{}) + newFolder := createFolderWithACL(t, sc.sqlStore, "NewFolder", sc.user, []folderACLItem{}) command := getCreateCommand(newFolder.Id, "Text - Library Panel") resp := sc.service.createHandler(sc.reqContext, command) var result = validateAndUnMarshalResponse(t, resp) diff --git a/pkg/services/librarypanels/librarypanels_permissions_test.go b/pkg/services/librarypanels/librarypanels_permissions_test.go index ee3b3008fdf..a8fdd1374ea 100644 --- a/pkg/services/librarypanels/librarypanels_permissions_test.go +++ b/pkg/services/librarypanels/librarypanels_permissions_test.go @@ -68,7 +68,7 @@ func TestLibraryPanelPermissions(t *testing.T) { for _, testCase := range accessCases { testScenario(t, fmt.Sprintf("When %s tries to create a library panel in a folder with %s, it should return correct status", testCase.role, testCase.desc), func(t *testing.T, sc scenarioContext) { - folder := createFolderWithACL(t, "Folder", sc.user, testCase.items) + folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items) sc.reqContext.SignedInUser.OrgRole = testCase.role command := getCreateCommand(folder.Id, "Library Panel Name") @@ -78,11 +78,11 @@ func TestLibraryPanelPermissions(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it to a folder with %s, it should return correct status", testCase.role, testCase.desc), func(t *testing.T, sc scenarioContext) { - fromFolder := createFolderWithACL(t, "Everyone", sc.user, everyonePermissions) + fromFolder := createFolderWithACL(t, sc.sqlStore, "Everyone", sc.user, everyonePermissions) command := getCreateCommand(fromFolder.Id, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, command) result := validateAndUnMarshalResponse(t, resp) - toFolder := createFolderWithACL(t, "Folder", sc.user, testCase.items) + toFolder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items) sc.reqContext.SignedInUser.OrgRole = testCase.role cmd := patchLibraryPanelCommand{FolderID: toFolder.Id, Version: 1} @@ -93,11 +93,11 @@ func TestLibraryPanelPermissions(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it from a folder with %s, it should return correct status", testCase.role, testCase.desc), func(t *testing.T, sc scenarioContext) { - fromFolder := createFolderWithACL(t, "Everyone", sc.user, testCase.items) + fromFolder := createFolderWithACL(t, sc.sqlStore, "Everyone", sc.user, testCase.items) command := getCreateCommand(fromFolder.Id, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, command) result := validateAndUnMarshalResponse(t, resp) - toFolder := createFolderWithACL(t, "Folder", sc.user, everyonePermissions) + toFolder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, everyonePermissions) sc.reqContext.SignedInUser.OrgRole = testCase.role cmd := patchLibraryPanelCommand{FolderID: toFolder.Id, Version: 1} @@ -108,7 +108,7 @@ func TestLibraryPanelPermissions(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to delete a library panel in a folder with %s, it should return correct status", testCase.role, testCase.desc), func(t *testing.T, sc scenarioContext) { - folder := createFolderWithACL(t, "Folder", sc.user, testCase.items) + folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items) cmd := getCreateCommand(folder.Id, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, cmd) result := validateAndUnMarshalResponse(t, resp) @@ -121,8 +121,8 @@ func TestLibraryPanelPermissions(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to connect a library panel in a folder with %s, it should return correct status", testCase.role, testCase.desc), func(t *testing.T, sc scenarioContext) { - folder := createFolderWithACL(t, "Folder", sc.user, testCase.items) - dashboard := createDashboard(t, sc.user, "Some Folder Dash", folder.Id) + folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items) + dashboard := createDashboard(t, sc.sqlStore, sc.user, "Some Folder Dash", folder.Id) cmd := getCreateCommand(folder.Id, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, cmd) result := validateAndUnMarshalResponse(t, resp) @@ -135,8 +135,8 @@ func TestLibraryPanelPermissions(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to disconnect a library panel in a folder with %s, it should return correct status", testCase.role, testCase.desc), func(t *testing.T, sc scenarioContext) { - folder := createFolderWithACL(t, "Folder", sc.user, testCase.items) - dashboard := createDashboard(t, sc.user, "Some Folder Dash", folder.Id) + folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items) + dashboard := createDashboard(t, sc.sqlStore, sc.user, "Some Folder Dash", folder.Id) cmd := getCreateCommand(folder.Id, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, cmd) result := validateAndUnMarshalResponse(t, resp) @@ -152,7 +152,7 @@ func TestLibraryPanelPermissions(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to delete all library panels in a folder with %s, it should return correct status", testCase.role, testCase.desc), func(t *testing.T, sc scenarioContext) { - folder := createFolderWithACL(t, "Folder", sc.user, testCase.items) + folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, testCase.items) cmd := getCreateCommand(folder.Id, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, cmd) validateAndUnMarshalResponse(t, resp) @@ -191,7 +191,7 @@ func TestLibraryPanelPermissions(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it to the General folder, it should return correct status", testCase.role), func(t *testing.T, sc scenarioContext) { - folder := createFolderWithACL(t, "Folder", sc.user, everyonePermissions) + folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, everyonePermissions) command := getCreateCommand(folder.Id, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, command) result := validateAndUnMarshalResponse(t, resp) @@ -205,7 +205,7 @@ func TestLibraryPanelPermissions(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it from the General folder, it should return correct status", testCase.role), func(t *testing.T, sc scenarioContext) { - folder := createFolderWithACL(t, "Folder", sc.user, everyonePermissions) + folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, everyonePermissions) command := getCreateCommand(0, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, command) result := validateAndUnMarshalResponse(t, resp) @@ -231,7 +231,7 @@ func TestLibraryPanelPermissions(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to connect a library panel in the General folder, it should return correct status", testCase.role), func(t *testing.T, sc scenarioContext) { - dashboard := createDashboard(t, sc.user, "General Folder Dash", 0) + dashboard := createDashboard(t, sc.sqlStore, sc.user, "General Folder Dash", 0) cmd := getCreateCommand(0, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, cmd) result := validateAndUnMarshalResponse(t, resp) @@ -244,7 +244,7 @@ func TestLibraryPanelPermissions(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to disconnect a library panel in the General folder, it should return correct status", testCase.role), func(t *testing.T, sc scenarioContext) { - dashboard := createDashboard(t, sc.user, "General Folder Dash", 0) + dashboard := createDashboard(t, sc.sqlStore, sc.user, "General Folder Dash", 0) cmd := getCreateCommand(0, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, cmd) result := validateAndUnMarshalResponse(t, resp) @@ -260,7 +260,7 @@ func TestLibraryPanelPermissions(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to get connected dashboards in the General folder for a library panel in the General folder, it should return correct status", testCase.role), func(t *testing.T, sc scenarioContext) { - dashboard := createDashboard(t, sc.user, "General Folder Dash", 0) + dashboard := createDashboard(t, sc.sqlStore, sc.user, "General Folder Dash", 0) cmd := getCreateCommand(0, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, cmd) result := validateAndUnMarshalResponse(t, resp) @@ -301,7 +301,7 @@ func TestLibraryPanelPermissions(t *testing.T) { testScenario(t, fmt.Sprintf("When %s tries to patch a library panel by moving it to a folder that doesn't exist, it should fail", testCase.role), func(t *testing.T, sc scenarioContext) { - folder := createFolderWithACL(t, "Folder", sc.user, everyonePermissions) + folder := createFolderWithACL(t, sc.sqlStore, "Folder", sc.user, everyonePermissions) command := getCreateCommand(folder.Id, "Library Panel Name") resp := sc.service.createHandler(sc.reqContext, command) result := validateAndUnMarshalResponse(t, resp) @@ -329,7 +329,7 @@ func TestLibraryPanelPermissions(t *testing.T) { func(t *testing.T, sc scenarioContext) { var results []libraryPanel for i, folderCase := range folderCases { - folder := createFolderWithACL(t, fmt.Sprintf("Folder%v", i), sc.user, folderCase) + folder := createFolderWithACL(t, sc.sqlStore, fmt.Sprintf("Folder%v", i), sc.user, folderCase) cmd := getCreateCommand(folder.Id, fmt.Sprintf("Library Panel in Folder%v", i)) resp := sc.service.createHandler(sc.reqContext, cmd) result := validateAndUnMarshalResponse(t, resp) @@ -401,8 +401,8 @@ func TestLibraryPanelPermissions(t *testing.T) { resp := sc.service.createHandler(sc.reqContext, cmd) result := validateAndUnMarshalResponse(t, resp) for i, folderCase := range folderCases { - folder := createFolderWithACL(t, fmt.Sprintf("Folder%v", i), sc.user, folderCase) - dashboard := createDashboard(t, sc.user, "Some Folder Dash", folder.Id) + folder := createFolderWithACL(t, sc.sqlStore, fmt.Sprintf("Folder%v", i), sc.user, folderCase) + dashboard := createDashboard(t, sc.sqlStore, sc.user, "Some Folder Dash", folder.Id) sc.reqContext.ReplaceAllParams(map[string]string{":uid": result.Result.UID, ":dashboardId": strconv.FormatInt(dashboard.Id, 10)}) resp = sc.service.connectHandler(sc.reqContext) require.Equal(t, 200, resp.Status()) @@ -434,7 +434,7 @@ func TestLibraryPanelPermissions(t *testing.T) { func(t *testing.T, sc scenarioContext) { var results []libraryPanel for i, folderCase := range folderCases { - folder := createFolderWithACL(t, fmt.Sprintf("Folder%v", i), sc.user, folderCase) + folder := createFolderWithACL(t, sc.sqlStore, fmt.Sprintf("Folder%v", i), sc.user, folderCase) cmd := getCreateCommand(folder.Id, fmt.Sprintf("Library Panel in Folder%v", i)) resp := sc.service.createHandler(sc.reqContext, cmd) result := validateAndUnMarshalResponse(t, resp) diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 501775ed83f..861bbc3559a 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -13,8 +13,8 @@ import ( "gopkg.in/macaron.v1" "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" + dboards "github.com/grafana/grafana/pkg/dashboards" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/dashboards" @@ -731,6 +731,7 @@ type scenarioContext struct { user models.SignedInUser folder *models.Folder initialResult libraryPanelResult + sqlStore *sqlstore.SQLStore } type folderACLItem struct { @@ -738,7 +739,8 @@ type folderACLItem struct { permission models.PermissionType } -func createDashboard(t *testing.T, user models.SignedInUser, title string, folderID int64) *models.Dashboard { +func createDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, user models.SignedInUser, title string, + folderID int64) *models.Dashboard { dash := models.NewDashboard(title) dash.FolderId = folderID dashItem := &dashboards.SaveDashboardDTO{ @@ -748,53 +750,47 @@ func createDashboard(t *testing.T, user models.SignedInUser, title string, folde User: &user, Overwrite: false, } - bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error { - return nil + origUpdateAlerting := dashboards.UpdateAlerting + t.Cleanup(func() { + dashboards.UpdateAlerting = origUpdateAlerting }) - bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error { - cmd.Result = &models.ValidateDashboardBeforeSaveResult{} + dashboards.UpdateAlerting = func(store dboards.Store, orgID int64, dashboard *models.Dashboard, + user *models.SignedInUser) error { return nil - }) - bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error { - cmd.Result = nil - return nil - }) - bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error { - return nil - }) + } - dashboard, err := dashboards.NewService().SaveDashboard(dashItem, true) + dashboard, err := dashboards.NewService(sqlStore).SaveDashboard(dashItem, true) require.NoError(t, err) return dashboard } -func createFolderWithACL(t *testing.T, title string, user models.SignedInUser, items []folderACLItem) *models.Folder { - s := dashboards.NewFolderService(user.OrgId, &user) - folderCmd := models.CreateFolderCommand{ - Uid: title, - Title: title, - } - err := s.CreateFolder(&folderCmd) +func createFolderWithACL(t *testing.T, sqlStore *sqlstore.SQLStore, title string, user models.SignedInUser, + items []folderACLItem) *models.Folder { + t.Helper() + + s := dashboards.NewFolderService(user.OrgId, &user, sqlStore) + t.Logf("Creating folder with title and UID %q", title) + folder, err := s.CreateFolder(title, title) require.NoError(t, err) - updateFolderACL(t, folderCmd.Result.Id, items) + updateFolderACL(t, sqlStore, folder.Id, items) - return folderCmd.Result + return folder } -func updateFolderACL(t *testing.T, folderID int64, items []folderACLItem) { +func updateFolderACL(t *testing.T, sqlStore *sqlstore.SQLStore, folderID int64, items []folderACLItem) { + t.Helper() + if len(items) == 0 { return } - cmd := models.UpdateDashboardAclCommand{ - DashboardID: folderID, - } + var aclItems []*models.DashboardAcl for _, item := range items { role := item.roleType permission := item.permission - cmd.Items = append(cmd.Items, &models.DashboardAcl{ + aclItems = append(aclItems, &models.DashboardAcl{ DashboardID: folderID, Role: &role, Permission: permission, @@ -803,11 +799,13 @@ func updateFolderACL(t *testing.T, folderID int64, items []folderACLItem) { }) } - err := bus.Dispatch(&cmd) + err := sqlStore.UpdateDashboardACL(folderID, aclItems) require.NoError(t, err) } func validateAndUnMarshalResponse(t *testing.T, resp response.Response) libraryPanelResult { + t.Helper() + require.Equal(t, 200, resp.Status()) var result = libraryPanelResult{} @@ -818,6 +816,8 @@ func validateAndUnMarshalResponse(t *testing.T, resp response.Response) libraryP } func scenarioWithLibraryPanel(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) { + t.Helper() + testScenario(t, desc, func(t *testing.T, sc scenarioContext) { command := getCreateCommand(sc.folder.Id, "Text - Library Panel") resp := sc.service.createHandler(sc.reqContext, command) @@ -865,25 +865,26 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo // deliberate difference between signed in user and user in db to make it crystal clear // what to expect in the tests // In the real world these are identical - cmd := &models.CreateUserCommand{ + cmd := models.CreateUserCommand{ Email: "user.in.db@test.com", Name: "User In DB", Login: UserInDbName, } - err := sqlstore.CreateUser(context.Background(), cmd) + _, err := sqlStore.CreateUser(context.Background(), cmd) require.NoError(t, err) sc := scenarioContext{ - user: user, - ctx: &ctx, - service: &service, + user: user, + ctx: &ctx, + service: &service, + sqlStore: sqlStore, reqContext: &models.ReqContext{ Context: &ctx, SignedInUser: &user, }, } - sc.folder = createFolderWithACL(t, "ScenarioFolder", sc.user, []folderACLItem{}) + sc.folder = createFolderWithACL(t, sc.sqlStore, "ScenarioFolder", sc.user, []folderACLItem{}) fn(t, sc) }) diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index 4bda59eb273..01a42776985 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -8,7 +8,7 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/plugins/manager" + "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/live/features" "github.com/grafana/grafana/pkg/setting" @@ -43,6 +43,7 @@ type GrafanaLive struct { Cfg *setting.Cfg `inject:""` RouteRegister routing.RouteRegister `inject:""` LogsService *cloudwatch.LogsService `inject:""` + PluginManager plugins.Manager `inject:""` node *centrifuge.Node // The websocket handler @@ -222,8 +223,8 @@ func (g *GrafanaLive) GetChannelHandlerFactory(scope string, name string) (model }, nil } - p, ok := manager.Plugins[name] - if ok { + p := g.PluginManager.GetPlugin(name) + if p != nil { h := &PluginHandler{ Plugin: p, } diff --git a/pkg/services/login/login.go b/pkg/services/login/login.go index 9e08a36b062..0e4ab3da5dc 100644 --- a/pkg/services/login/login.go +++ b/pkg/services/login/login.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/quota" + "github.com/grafana/grafana/pkg/services/sqlstore" ) func init() { @@ -21,6 +22,7 @@ var ( type TeamSyncFunc func(user *models.User, externalUser *models.ExternalUserInfo) error type LoginService struct { + SQLStore *sqlstore.SQLStore `inject:""` Bus bus.Bus `inject:""` QuotaService *quota.QuotaService `inject:""` TeamSync TeamSyncFunc @@ -107,7 +109,7 @@ func (ls *LoginService) UpsertUser(cmd *models.UpsertUserCommand) error { // Sync isGrafanaAdmin permission if extUser.IsGrafanaAdmin != nil && *extUser.IsGrafanaAdmin != cmd.Result.IsAdmin { - if err := ls.Bus.Dispatch(&models.UpdateUserPermissionsCommand{UserId: cmd.Result.Id, IsGrafanaAdmin: *extUser.IsGrafanaAdmin}); err != nil { + if err := ls.SQLStore.UpdateUserPermissions(cmd.Result.Id, *extUser.IsGrafanaAdmin); err != nil { return err } } diff --git a/pkg/services/provisioning/dashboards/dashboard.go b/pkg/services/provisioning/dashboards/dashboard.go index f15903d336c..39eb9c89fc8 100644 --- a/pkg/services/provisioning/dashboards/dashboard.go +++ b/pkg/services/provisioning/dashboards/dashboard.go @@ -6,8 +6,10 @@ import ( "os" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/dashboards" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/util/errutil" ) @@ -22,7 +24,7 @@ type DashboardProvisioner interface { } // DashboardProvisionerFactory creates DashboardProvisioners based on input -type DashboardProvisionerFactory func(string) (DashboardProvisioner, error) +type DashboardProvisionerFactory func(string, dashboards.Store, plugins.DataRequestHandler) (DashboardProvisioner, error) // Provisioner is responsible for syncing dashboard from disk to Grafana's database. type Provisioner struct { @@ -32,16 +34,15 @@ type Provisioner struct { } // New returns a new DashboardProvisioner -func New(configDirectory string) (*Provisioner, error) { +func New(configDirectory string, store dashboards.Store, reqHandler plugins.DataRequestHandler) (DashboardProvisioner, error) { logger := log.New("provisioning.dashboard") cfgReader := &configReader{path: configDirectory, log: logger} configs, err := cfgReader.readConfig() - if err != nil { return nil, errutil.Wrap("Failed to read dashboards config", err) } - fileReaders, err := getFileReaders(configs, logger) + fileReaders, err := getFileReaders(configs, logger, store) if err != nil { return nil, errutil.Wrap("Failed to initialize file readers", err) } @@ -115,13 +116,14 @@ func (provider *Provisioner) GetAllowUIUpdatesFromConfig(name string) bool { return false } -func getFileReaders(configs []*config, logger log.Logger) ([]*FileReader, error) { +func getFileReaders(configs []*config, logger log.Logger, store dashboards.Store) ([]*FileReader, error) { var readers []*FileReader for _, config := range configs { switch config.Type { case "file": - fileReader, err := NewDashboardFileReader(config, logger.New("type", config.Type, "name", config.Name)) + fileReader, err := NewDashboardFileReader(config, logger.New("type", config.Type, "name", config.Name), + store) if err != nil { return nil, errutil.Wrapf(err, "Failed to create file reader for config %v", config.Name) } diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 32ebd3bb9a3..b448d609176 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" + dboards "github.com/grafana/grafana/pkg/dashboards" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" @@ -35,7 +36,7 @@ type FileReader struct { } // NewDashboardFileReader returns a new filereader based on `config` -func NewDashboardFileReader(cfg *config, log log.Logger) (*FileReader, error) { +func NewDashboardFileReader(cfg *config, log log.Logger, store dboards.Store) (*FileReader, error) { var path string path, ok := cfg.Options["path"].(string) if !ok { @@ -56,7 +57,7 @@ func NewDashboardFileReader(cfg *config, log log.Logger) (*FileReader, error) { Cfg: cfg, Path: path, log: log, - dashboardProvisioningService: dashboards.NewProvisioningService(), + dashboardProvisioningService: dashboards.NewProvisioningService(store), FoldersFromFilesStructure: foldersFromFilesStructure, }, nil } diff --git a/pkg/services/provisioning/dashboards/file_reader_linux_test.go b/pkg/services/provisioning/dashboards/file_reader_linux_test.go index a575d5daa03..b32c642551b 100644 --- a/pkg/services/provisioning/dashboards/file_reader_linux_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_linux_test.go @@ -24,7 +24,7 @@ func TestProvisionedSymlinkedFolder(t *testing.T) { Options: map[string]interface{}{"path": symlinkedFolder}, } - reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) + reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil) require.NoError(t, err) want, err := filepath.Abs(containingID) diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index c741cea60ff..0093d67b691 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/grafana/grafana/pkg/bus" + dboards "github.com/grafana/grafana/pkg/dashboards" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/util" @@ -41,14 +42,14 @@ func TestCreatingNewDashboardFileReader(t *testing.T) { Convey("using path parameter", func() { cfg.Options["path"] = defaultDashboards - reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) + reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil) So(err, ShouldBeNil) So(reader.Path, ShouldNotEqual, "") }) Convey("using folder as options", func() { cfg.Options["folder"] = defaultDashboards - reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) + reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil) So(err, ShouldBeNil) So(reader.Path, ShouldNotEqual, "") }) @@ -56,7 +57,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) { Convey("using foldersFromFilesStructure as options", func() { cfg.Options["path"] = foldersFromFilesStructure cfg.Options["foldersFromFilesStructure"] = true - reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) + reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil) So(err, ShouldBeNil) So(reader.Path, ShouldNotEqual, "") }) @@ -68,7 +69,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) { } cfg.Options["folder"] = fullPath - reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) + reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil) So(err, ShouldBeNil) So(reader.Path, ShouldEqual, fullPath) @@ -77,7 +78,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) { Convey("using relative path", func() { cfg.Options["folder"] = defaultDashboards - reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) + reader, err := NewDashboardFileReader(cfg, log.New("test-logger"), nil) So(err, ShouldBeNil) resolvedPath := reader.resolvedPath() @@ -111,7 +112,7 @@ func TestDashboardFileReader(t *testing.T) { cfg.Options["path"] = defaultDashboards cfg.Folder = "Team A" - reader, err := NewDashboardFileReader(cfg, logger) + reader, err := NewDashboardFileReader(cfg, logger, nil) So(err, ShouldBeNil) err = reader.walkDisk() @@ -142,7 +143,7 @@ func TestDashboardFileReader(t *testing.T) { Slug: "grafana", }) - reader, err := NewDashboardFileReader(cfg, logger) + reader, err := NewDashboardFileReader(cfg, logger, nil) So(err, ShouldBeNil) err = reader.walkDisk() @@ -154,7 +155,7 @@ func TestDashboardFileReader(t *testing.T) { Convey("Overrides id from dashboard.json files", func() { cfg.Options["path"] = containingID - reader, err := NewDashboardFileReader(cfg, logger) + reader, err := NewDashboardFileReader(cfg, logger, nil) So(err, ShouldBeNil) err = reader.walkDisk() @@ -167,7 +168,7 @@ func TestDashboardFileReader(t *testing.T) { cfg.Options["path"] = foldersFromFilesStructure cfg.Options["foldersFromFilesStructure"] = true - reader, err := NewDashboardFileReader(cfg, logger) + reader, err := NewDashboardFileReader(cfg, logger, nil) So(err, ShouldBeNil) err = reader.walkDisk() @@ -211,14 +212,14 @@ func TestDashboardFileReader(t *testing.T) { Folder: "", } - _, err := NewDashboardFileReader(cfg, logger) + _, err := NewDashboardFileReader(cfg, logger, nil) So(err, ShouldNotBeNil) }) Convey("Broken dashboards should not cause error", func() { cfg.Options["path"] = brokenDashboards - _, err := NewDashboardFileReader(cfg, logger) + _, err := NewDashboardFileReader(cfg, logger, nil) So(err, ShouldBeNil) }) @@ -226,13 +227,13 @@ func TestDashboardFileReader(t *testing.T) { cfg1 := &config{Name: "1", Type: "file", OrgID: 1, Folder: "f1", Options: map[string]interface{}{"path": containingID}} cfg2 := &config{Name: "2", Type: "file", OrgID: 1, Folder: "f2", Options: map[string]interface{}{"path": containingID}} - reader1, err := NewDashboardFileReader(cfg1, logger) + reader1, err := NewDashboardFileReader(cfg1, logger, nil) So(err, ShouldBeNil) err = reader1.walkDisk() So(err, ShouldBeNil) - reader2, err := NewDashboardFileReader(cfg2, logger) + reader2, err := NewDashboardFileReader(cfg2, logger, nil) So(err, ShouldBeNil) err = reader2.walkDisk() @@ -336,7 +337,7 @@ func TestDashboardFileReader(t *testing.T) { Convey("Missing dashboard should be unprovisioned if DisableDeletion = true", func() { cfg.DisableDeletion = true - reader, err := NewDashboardFileReader(cfg, logger) + reader, err := NewDashboardFileReader(cfg, logger, nil) So(err, ShouldBeNil) err = reader.walkDisk() @@ -347,7 +348,7 @@ func TestDashboardFileReader(t *testing.T) { }) Convey("Missing dashboard should be deleted if DisableDeletion = false", func() { - reader, err := NewDashboardFileReader(cfg, logger) + reader, err := NewDashboardFileReader(cfg, logger, nil) So(err, ShouldBeNil) err = reader.walkDisk() @@ -395,7 +396,7 @@ func mockDashboardProvisioningService() *fakeDashboardProvisioningService { mock := fakeDashboardProvisioningService{ provisioned: map[string][]*models.DashboardProvisioning{}, } - dashboards.NewProvisioningService = func() dashboards.DashboardProvisioningService { + dashboards.NewProvisioningService = func(dboards.Store) dashboards.DashboardProvisioningService { return &mock } return &mock diff --git a/pkg/services/provisioning/plugins/config_reader.go b/pkg/services/provisioning/plugins/config_reader.go index 40550c8bdee..69f07bfcbd4 100644 --- a/pkg/services/provisioning/plugins/config_reader.go +++ b/pkg/services/provisioning/plugins/config_reader.go @@ -55,7 +55,7 @@ func (cr *configReaderImpl) readConfig(path string) ([]*pluginsAsConfig, error) checkOrgIDAndOrgName(apps) - err = validatePluginsConfig(apps) + err = cr.validatePluginsConfig(apps) if err != nil { return nil, err } @@ -105,7 +105,7 @@ func validateRequiredField(apps []*pluginsAsConfig) error { return nil } -func validatePluginsConfig(apps []*pluginsAsConfig) error { +func (cr *configReaderImpl) validatePluginsConfig(apps []*pluginsAsConfig) error { for i := range apps { if apps[i].Apps == nil { continue diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index 1c9a3003611..61a96507149 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -6,11 +6,13 @@ import ( "sync" "github.com/grafana/grafana/pkg/infra/log" + plugifaces "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/provisioning/dashboards" "github.com/grafana/grafana/pkg/services/provisioning/datasources" "github.com/grafana/grafana/pkg/services/provisioning/notifiers" "github.com/grafana/grafana/pkg/services/provisioning/plugins" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util/errutil" ) @@ -27,10 +29,8 @@ type ProvisioningService interface { func init() { registry.Register(®istry.Descriptor{ Name: "ProvisioningService", - Instance: NewProvisioningServiceImpl( - func(path string) (dashboards.DashboardProvisioner, error) { - return dashboards.New(path) - }, + Instance: newProvisioningServiceImpl( + dashboards.New, notifiers.Provision, datasources.Provision, plugins.Provision, @@ -39,7 +39,7 @@ func init() { }) } -func NewProvisioningServiceImpl( +func newProvisioningServiceImpl( newDashboardProvisioner dashboards.DashboardProvisionerFactory, provisionNotifiers func(string) error, provisionDatasources func(string) error, @@ -55,7 +55,9 @@ func NewProvisioningServiceImpl( } type provisioningServiceImpl struct { - Cfg *setting.Cfg `inject:""` + Cfg *setting.Cfg `inject:""` + RequestHandler plugifaces.DataRequestHandler `inject:""` + SQLStore *sqlstore.SQLStore `inject:""` log log.Logger pollingCtxCancel context.CancelFunc newDashboardProvisioner dashboards.DashboardProvisionerFactory @@ -134,7 +136,7 @@ func (ps *provisioningServiceImpl) ProvisionNotifications() error { func (ps *provisioningServiceImpl) ProvisionDashboards() error { dashboardPath := filepath.Join(ps.Cfg.ProvisioningPath, "dashboards") - dashProvisioner, err := ps.newDashboardProvisioner(dashboardPath) + dashProvisioner, err := ps.newDashboardProvisioner(dashboardPath, ps.SQLStore, ps.RequestHandler) if err != nil { return errutil.Wrap("Failed to create provisioner", err) } diff --git a/pkg/services/provisioning/provisioning_test.go b/pkg/services/provisioning/provisioning_test.go index e88c4861f54..67ee77d8b3d 100644 --- a/pkg/services/provisioning/provisioning_test.go +++ b/pkg/services/provisioning/provisioning_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + dboards "github.com/grafana/grafana/pkg/dashboards" + plugifaces "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/provisioning/dashboards" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" @@ -90,8 +92,8 @@ func setup() *serviceTestStruct { pollChangesChannel <- ctx } - serviceTest.service = NewProvisioningServiceImpl( - func(path string) (dashboards.DashboardProvisioner, error) { + serviceTest.service = newProvisioningServiceImpl( + func(string, dboards.Store, plugifaces.DataRequestHandler) (dashboards.DashboardProvisioner, error) { return serviceTest.mock, nil }, nil, diff --git a/pkg/services/rendering/rendering.go b/pkg/services/rendering/rendering.go index b23804b8553..d9a65297d4c 100644 --- a/pkg/services/rendering/rendering.go +++ b/pkg/services/rendering/rendering.go @@ -17,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -50,6 +49,7 @@ type RenderingService struct { Cfg *setting.Cfg `inject:""` RemoteCacheService *remotecache.RemoteCache `inject:""` + PluginManager plugins.Manager `inject:""` } func (rs *RenderingService) Init() error { @@ -87,7 +87,7 @@ func (rs *RenderingService) Run(ctx context.Context) error { if rs.pluginAvailable() { rs.log = rs.log.New("renderer", "plugin") - rs.pluginInfo = manager.Renderer + rs.pluginInfo = rs.PluginManager.Renderer() if err := rs.startPlugin(ctx); err != nil { return err @@ -107,7 +107,7 @@ func (rs *RenderingService) Run(ctx context.Context) error { } func (rs *RenderingService) pluginAvailable() bool { - return manager.Renderer != nil + return rs.PluginManager.Renderer() != nil } func (rs *RenderingService) remoteAvailable() bool { diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 12f955a9ac2..88427412655 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -2,6 +2,7 @@ package sqlstore import ( "bytes" + "context" "fmt" "strings" "time" @@ -166,18 +167,18 @@ func deleteAlertDefinition(dashboardId int64, sess *DBSession) error { return nil } -func SaveAlerts(cmd *models.SaveAlertsCommand) error { - return inTransaction(func(sess *DBSession) error { - existingAlerts, err := GetAlertsByDashboardId2(cmd.DashboardId, sess) +func (ss *SQLStore) SaveAlerts(dashID int64, alerts []*models.Alert) error { + return ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error { + existingAlerts, err := GetAlertsByDashboardId2(dashID, sess) if err != nil { return err } - if err := updateAlerts(existingAlerts, cmd, sess); err != nil { + if err := updateAlerts(existingAlerts, alerts, sess); err != nil { return err } - if err := deleteMissingAlerts(existingAlerts, cmd, sess); err != nil { + if err := deleteMissingAlerts(existingAlerts, alerts, sess); err != nil { return err } @@ -185,8 +186,27 @@ func SaveAlerts(cmd *models.SaveAlertsCommand) error { }) } -func updateAlerts(existingAlerts []*models.Alert, cmd *models.SaveAlertsCommand, sess *DBSession) error { - for _, alert := range cmd.Alerts { +func SaveAlerts(cmd *models.SaveAlertsCommand) error { + return inTransaction(func(sess *DBSession) error { + existingAlerts, err := GetAlertsByDashboardId2(cmd.DashboardId, sess) + if err != nil { + return err + } + + if err := updateAlerts(existingAlerts, cmd.Alerts, sess); err != nil { + return err + } + + if err := deleteMissingAlerts(existingAlerts, cmd.Alerts, sess); err != nil { + return err + } + + return nil + }) +} + +func updateAlerts(existingAlerts []*models.Alert, alerts []*models.Alert, sess *DBSession) error { + for _, alert := range alerts { update := false var alertToUpdate *models.Alert @@ -245,11 +265,11 @@ func updateAlerts(existingAlerts []*models.Alert, cmd *models.SaveAlertsCommand, return nil } -func deleteMissingAlerts(alerts []*models.Alert, cmd *models.SaveAlertsCommand, sess *DBSession) error { +func deleteMissingAlerts(alerts []*models.Alert, existingAlerts []*models.Alert, sess *DBSession) error { for _, missingAlert := range alerts { missing := true - for _, k := range cmd.Alerts { + for _, k := range existingAlerts { if missingAlert.PanelId == k.PanelId { missing = false break diff --git a/pkg/services/sqlstore/alert_test.go b/pkg/services/sqlstore/alert_test.go index 3b8b0e8da6d..d38408f3c34 100644 --- a/pkg/services/sqlstore/alert_test.go +++ b/pkg/services/sqlstore/alert_test.go @@ -30,10 +30,11 @@ func TestAlertingDataAccess(t *testing.T) { defer resetTimeNow() Convey("Testing Alerting data access", t, func() { - InitTestDB(t) + sqlStore := InitTestDB(t) - testDash := insertTestDashboard(t, "dashboard with alerts", 1, 0, false, "alert") - evalData, _ := simplejson.NewJson([]byte(`{"test": "test"}`)) + testDash := insertTestDashboard(t, sqlStore, "dashboard with alerts", 1, 0, false, "alert") + evalData, err := simplejson.NewJson([]byte(`{"test": "test"}`)) + So(err, ShouldBeNil) items := []*models.Alert{ { PanelId: 1, @@ -54,7 +55,7 @@ func TestAlertingDataAccess(t *testing.T) { UserId: 1, } - err := SaveAlerts(&cmd) + err = SaveAlerts(&cmd) Convey("Can create one alert", func() { So(err, ShouldBeNil) @@ -271,10 +272,11 @@ func TestPausingAlerts(t *testing.T) { defer resetTimeNow() Convey("Given an alert", t, func() { - InitTestDB(t) + sqlStore := InitTestDB(t) - testDash := insertTestDashboard(t, "dashboard with alerts", 1, 0, false, "alert") - alert, _ := insertTestAlert("Alerting title", "Alerting message", testDash.OrgId, testDash.Id, simplejson.New()) + testDash := insertTestDashboard(t, sqlStore, "dashboard with alerts", 1, 0, false, "alert") + alert, err := insertTestAlert("Alerting title", "Alerting message", testDash.OrgId, testDash.Id, simplejson.New()) + So(err, ShouldBeNil) stateDateBeforePause := alert.NewStateDate stateDateAfterPause := stateDateBeforePause diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 12a4c52f2e7..5ccf554e599 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -1,6 +1,8 @@ package sqlstore import ( + "context" + "fmt" "strings" "time" @@ -24,7 +26,6 @@ var shadowSearchCounter = prometheus.NewCounterVec( ) func init() { - bus.AddHandler("sql", SaveDashboard) bus.AddHandler("sql", GetDashboard) bus.AddHandler("sql", GetDashboards) bus.AddHandler("sql", DeleteDashboard) @@ -35,7 +36,6 @@ func init() { bus.AddHandler("sql", GetDashboardsByPluginId) bus.AddHandler("sql", GetDashboardPermissionsForUser) bus.AddHandler("sql", GetDashboardsBySlug) - bus.AddHandler("sql", ValidateDashboardBeforeSave) bus.AddHandler("sql", HasEditPermissionInFolders) bus.AddHandler("sql", HasAdminPermissionInFolders) @@ -44,10 +44,11 @@ func init() { var generateNewUid func() string = util.GenerateShortUID -func SaveDashboard(cmd *models.SaveDashboardCommand) error { - return inTransaction(func(sess *DBSession) error { - return saveDashboard(sess, cmd) +func (ss *SQLStore) SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error) { + err := ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error { + return saveDashboard(sess, &cmd) }) + return cmd.Result, err } func saveDashboard(sess *DBSession, cmd *models.SaveDashboardCommand) error { @@ -162,7 +163,7 @@ func saveDashboard(sess *DBSession, cmd *models.SaveDashboardCommand) error { cmd.Result = dash - return err + return nil } func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) { @@ -182,6 +183,26 @@ func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) { return "", models.ErrDashboardFailedGenerateUniqueUid } +// GetDashboard gets a dashboard. +func (ss *SQLStore) GetDashboard(id, orgID int64, uid, slug string) (*models.Dashboard, error) { + if id == 0 && slug == "" && uid == "" { + return nil, models.ErrDashboardIdentifierNotSet + } + + dashboard := models.Dashboard{Slug: slug, OrgId: orgID, Id: id, Uid: uid} + has, err := ss.engine.Get(&dashboard) + if err != nil { + return nil, err + } else if !has { + return nil, models.ErrDashboardNotFound + } + + dashboard.SetId(dashboard.Id) + dashboard.SetUid(dashboard.Uid) + return &dashboard, nil +} + +// TODO: Remove me func GetDashboard(query *models.GetDashboardQuery) error { if query.Id == 0 && len(query.Slug) == 0 && len(query.Uid) == 0 { return models.ErrDashboardIdentifierNotSet @@ -573,20 +594,20 @@ func GetDashboardUIDById(query *models.GetDashboardRefByIdQuery) error { return nil } -func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *models.ValidateDashboardBeforeSaveCommand) (err error) { - dash := cmd.Dashboard - +func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, dash *models.Dashboard, overwrite bool) (bool, error) { dashWithIdExists := false + isParentFolderChanged := false var existingById models.Dashboard if dash.Id > 0 { + var err error dashWithIdExists, err = sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existingById) if err != nil { - return err + return isParentFolderChanged, fmt.Errorf("SQL query for existing dashboard by ID failed: %w", err) } if !dashWithIdExists { - return models.ErrDashboardNotFound + return isParentFolderChanged, models.ErrDashboardNotFound } if dash.Uid == "" { @@ -598,30 +619,32 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *models.Validat var existingByUid models.Dashboard if dash.Uid != "" { + var err error dashWithUidExists, err = sess.Where("org_id=? AND uid=?", dash.OrgId, dash.Uid).Get(&existingByUid) if err != nil { - return err + return isParentFolderChanged, fmt.Errorf("SQL query for existing dashboard by UID failed: %w", err) } } if dash.FolderId > 0 { var existingFolder models.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 + folderExists, err := sess.Where("org_id=? AND id=? AND is_folder=?", dash.OrgId, dash.FolderId, + dialect.BooleanStr(true)).Get(&existingFolder) + if err != nil { + return isParentFolderChanged, fmt.Errorf("SQL query for folder failed: %w", err) } if !folderExists { - return models.ErrDashboardFolderNotFound + return isParentFolderChanged, models.ErrDashboardFolderNotFound } } if !dashWithIdExists && !dashWithUidExists { - return nil + return isParentFolderChanged, nil } if dashWithIdExists && dashWithUidExists && existingById.Id != existingByUid.Id { - return models.ErrDashboardWithSameUIDExists + return isParentFolderChanged, models.ErrDashboardWithSameUIDExists } existing := existingById @@ -632,84 +655,92 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *models.Validat existing = existingByUid if !dash.IsFolder { - cmd.Result.IsParentFolderChanged = true + isParentFolderChanged = true } } if (existing.IsFolder && !dash.IsFolder) || (!existing.IsFolder && dash.IsFolder) { - return models.ErrDashboardTypeMismatch + return isParentFolderChanged, models.ErrDashboardTypeMismatch } if !dash.IsFolder && dash.FolderId != existing.FolderId { - cmd.Result.IsParentFolderChanged = true + isParentFolderChanged = true } // check for is someone else has written in between if dash.Version != existing.Version { - if cmd.Overwrite { + if overwrite { dash.SetVersion(existing.Version) } else { - return models.ErrDashboardVersionMismatch + return isParentFolderChanged, models.ErrDashboardVersionMismatch } } // do not allow plugin dashboard updates without overwrite flag - if existing.PluginId != "" && !cmd.Overwrite { - return models.UpdatePluginDashboardError{PluginId: existing.PluginId} + if existing.PluginId != "" && !overwrite { + return isParentFolderChanged, models.UpdatePluginDashboardError{PluginId: existing.PluginId} } - return nil + return isParentFolderChanged, nil } -func getExistingDashboardByTitleAndFolder(sess *DBSession, cmd *models.ValidateDashboardBeforeSaveCommand) error { - dash := cmd.Dashboard +func getExistingDashboardByTitleAndFolder(sess *DBSession, dash *models.Dashboard, overwrite, + isParentFolderChanged bool) (bool, error) { var existing models.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) + 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 + return isParentFolderChanged, fmt.Errorf("SQL query for existing dashboard by org ID or folder ID failed: %w", err) } if exists && dash.Id != existing.Id { if existing.IsFolder && !dash.IsFolder { - return models.ErrDashboardWithSameNameAsFolder + return isParentFolderChanged, models.ErrDashboardWithSameNameAsFolder } if !existing.IsFolder && dash.IsFolder { - return models.ErrDashboardFolderWithSameNameAsDashboard + return isParentFolderChanged, models.ErrDashboardFolderWithSameNameAsDashboard } if !dash.IsFolder && (dash.FolderId != existing.FolderId || dash.Id == 0) { - cmd.Result.IsParentFolderChanged = true + isParentFolderChanged = true } - if cmd.Overwrite { + if overwrite { dash.SetId(existing.Id) dash.SetUid(existing.Uid) dash.SetVersion(existing.Version) } else { - return models.ErrDashboardWithSameNameInFolderExists + return isParentFolderChanged, models.ErrDashboardWithSameNameInFolderExists } } - return nil + return isParentFolderChanged, nil } -func ValidateDashboardBeforeSave(cmd *models.ValidateDashboardBeforeSaveCommand) (err error) { - cmd.Result = &models.ValidateDashboardBeforeSaveResult{} - - return inTransaction(func(sess *DBSession) error { - if err = getExistingDashboardByIdOrUidForUpdate(sess, cmd); err != nil { +func (ss *SQLStore) ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error) { + isParentFolderChanged := false + err := ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error { + var err error + isParentFolderChanged, err = getExistingDashboardByIdOrUidForUpdate(sess, dashboard, overwrite) + if err != nil { return err } - if err = getExistingDashboardByTitleAndFolder(sess, cmd); err != nil { + isParentFolderChanged, err = getExistingDashboardByTitleAndFolder(sess, dashboard, overwrite, + isParentFolderChanged) + if err != nil { return err } return nil }) + if err != nil { + return false, err + } + + return isParentFolderChanged, nil } func HasEditPermissionInFolders(query *models.HasEditPermissionInFoldersQuery) error { @@ -719,7 +750,8 @@ func HasEditPermissionInFolders(query *models.HasEditPermissionInFoldersQuery) e } builder := &SQLBuilder{} - builder.Write("SELECT COUNT(dashboard.id) AS count FROM dashboard WHERE dashboard.org_id = ? AND dashboard.is_folder = ?", query.SignedInUser.OrgId, dialect.BooleanStr(true)) + builder.Write("SELECT COUNT(dashboard.id) AS count FROM dashboard WHERE dashboard.org_id = ? AND dashboard.is_folder = ?", + query.SignedInUser.OrgId, dialect.BooleanStr(true)) builder.WriteDashboardPermissionFilter(query.SignedInUser, models.PERMISSION_EDIT) type folderCount struct { diff --git a/pkg/services/sqlstore/dashboard_acl.go b/pkg/services/sqlstore/dashboard_acl.go index 0b59769fb8c..2cf15c4fe2a 100644 --- a/pkg/services/sqlstore/dashboard_acl.go +++ b/pkg/services/sqlstore/dashboard_acl.go @@ -1,24 +1,26 @@ package sqlstore import ( + "context" + "fmt" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" ) func init() { - bus.AddHandler("sql", UpdateDashboardAcl) bus.AddHandler("sql", GetDashboardAclInfoList) } -func UpdateDashboardAcl(cmd *models.UpdateDashboardAclCommand) error { - return inTransaction(func(sess *DBSession) error { +func (ss *SQLStore) UpdateDashboardACL(dashboardID int64, items []*models.DashboardAcl) error { + return ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error { // delete existing items - _, err := sess.Exec("DELETE FROM dashboard_acl WHERE dashboard_id=?", cmd.DashboardID) + _, err := sess.Exec("DELETE FROM dashboard_acl WHERE dashboard_id=?", dashboardID) if err != nil { - return err + return fmt.Errorf("deleting from dashboard_acl failed: %w", err) } - for _, item := range cmd.Items { + for _, item := range items { if item.UserID == 0 && item.TeamID == 0 && (item.Role == nil || !item.Role.IsValid()) { return models.ErrDashboardAclInfoMissing } @@ -35,7 +37,7 @@ func UpdateDashboardAcl(cmd *models.UpdateDashboardAclCommand) error { // Update dashboard HasAcl flag dashboard := models.Dashboard{HasAcl: true} - _, err = sess.Cols("has_acl").Where("id=?", cmd.DashboardID).Update(&dashboard) + _, err = sess.Cols("has_acl").Where("id=?", dashboardID).Update(&dashboard) return err }) } diff --git a/pkg/services/sqlstore/dashboard_acl_test.go b/pkg/services/sqlstore/dashboard_acl_test.go index c66da74ed3c..9856e316bf6 100644 --- a/pkg/services/sqlstore/dashboard_acl_test.go +++ b/pkg/services/sqlstore/dashboard_acl_test.go @@ -11,14 +11,15 @@ import ( func TestDashboardAclDataAccess(t *testing.T) { Convey("Testing DB", t, func() { - InitTestDB(t) + sqlStore := InitTestDB(t) + Convey("Given a dashboard folder and a user", func() { - currentUser := createUser(t, "viewer", "Viewer", false) - savedFolder := insertTestDashboard(t, "1 test dash folder", 1, 0, true, "prod", "webapp") - childDash := insertTestDashboard(t, "2 test dash", 1, savedFolder.Id, false, "prod", "webapp") + currentUser := createUser(t, sqlStore, "viewer", "Viewer", false) + savedFolder := insertTestDashboard(t, sqlStore, "1 test dash folder", 1, 0, true, "prod", "webapp") + childDash := insertTestDashboard(t, sqlStore, "2 test dash", 1, savedFolder.Id, false, "prod", "webapp") Convey("When adding dashboard permission with userId and teamId set to 0", func() { - err := testHelperUpdateDashboardAcl(savedFolder.Id, models.DashboardAcl{ + err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{ OrgID: 1, DashboardID: savedFolder.Id, Permission: models.PERMISSION_EDIT, @@ -61,10 +62,7 @@ func TestDashboardAclDataAccess(t *testing.T) { }) Convey("Given dashboard folder with removed default permissions", func() { - err := UpdateDashboardAcl(&models.UpdateDashboardAclCommand{ - DashboardID: savedFolder.Id, - Items: []*models.DashboardAcl{}, - }) + err := sqlStore.UpdateDashboardACL(savedFolder.Id, nil) So(err, ShouldBeNil) Convey("When reading dashboard acl should return no acl items", func() { @@ -78,7 +76,7 @@ func TestDashboardAclDataAccess(t *testing.T) { }) Convey("Given dashboard folder permission", func() { - err := testHelperUpdateDashboardAcl(savedFolder.Id, models.DashboardAcl{ + err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{ OrgID: 1, UserID: currentUser.Id, DashboardID: savedFolder.Id, @@ -97,7 +95,7 @@ func TestDashboardAclDataAccess(t *testing.T) { }) Convey("Given child dashboard permission", func() { - err := testHelperUpdateDashboardAcl(childDash.Id, models.DashboardAcl{ + err := testHelperUpdateDashboardAcl(t, sqlStore, childDash.Id, models.DashboardAcl{ OrgID: 1, UserID: currentUser.Id, DashboardID: childDash.Id, @@ -121,7 +119,7 @@ func TestDashboardAclDataAccess(t *testing.T) { }) Convey("Given child dashboard permission in folder with no permissions", func() { - err := testHelperUpdateDashboardAcl(childDash.Id, models.DashboardAcl{ + err := testHelperUpdateDashboardAcl(t, sqlStore, childDash.Id, models.DashboardAcl{ OrgID: 1, UserID: currentUser.Id, DashboardID: childDash.Id, @@ -149,7 +147,7 @@ func TestDashboardAclDataAccess(t *testing.T) { }) Convey("Should be able to add dashboard permission", func() { - err := testHelperUpdateDashboardAcl(savedFolder.Id, models.DashboardAcl{ + err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{ OrgID: 1, UserID: currentUser.Id, DashboardID: savedFolder.Id, @@ -169,7 +167,7 @@ func TestDashboardAclDataAccess(t *testing.T) { So(q1.Result[0].UserEmail, ShouldEqual, currentUser.Email) Convey("Should be able to delete an existing permission", func() { - err := testHelperUpdateDashboardAcl(savedFolder.Id) + err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id) So(err, ShouldBeNil) q3 := &models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} @@ -180,14 +178,13 @@ func TestDashboardAclDataAccess(t *testing.T) { }) Convey("Given a team", func() { - group1 := models.CreateTeamCommand{Name: "group1 name", OrgId: 1} - err := CreateTeam(&group1) + team1, err := sqlStore.CreateTeam("group1 name", "", 1) So(err, ShouldBeNil) Convey("Should be able to add a user permission for a team", func() { - err := testHelperUpdateDashboardAcl(savedFolder.Id, models.DashboardAcl{ + err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{ OrgID: 1, - TeamID: group1.Result.Id, + TeamID: team1.Id, DashboardID: savedFolder.Id, Permission: models.PERMISSION_EDIT, }) @@ -198,13 +195,13 @@ func TestDashboardAclDataAccess(t *testing.T) { So(err, ShouldBeNil) So(q1.Result[0].DashboardId, ShouldEqual, savedFolder.Id) So(q1.Result[0].Permission, ShouldEqual, models.PERMISSION_EDIT) - So(q1.Result[0].TeamId, ShouldEqual, group1.Result.Id) + So(q1.Result[0].TeamId, ShouldEqual, team1.Id) }) Convey("Should be able to update an existing permission for a team", func() { - err := testHelperUpdateDashboardAcl(savedFolder.Id, models.DashboardAcl{ + err := testHelperUpdateDashboardAcl(t, sqlStore, savedFolder.Id, models.DashboardAcl{ OrgID: 1, - TeamID: group1.Result.Id, + TeamID: team1.Id, DashboardID: savedFolder.Id, Permission: models.PERMISSION_ADMIN, }) @@ -216,7 +213,7 @@ func TestDashboardAclDataAccess(t *testing.T) { So(len(q3.Result), ShouldEqual, 1) So(q3.Result[0].DashboardId, ShouldEqual, savedFolder.Id) So(q3.Result[0].Permission, ShouldEqual, models.PERMISSION_ADMIN) - So(q3.Result[0].TeamId, ShouldEqual, group1.Result.Id) + So(q3.Result[0].TeamId, ShouldEqual, team1.Id) }) }) }) diff --git a/pkg/services/sqlstore/dashboard_folder_test.go b/pkg/services/sqlstore/dashboard_folder_test.go index 572014b3829..b8dd72a9c9a 100644 --- a/pkg/services/sqlstore/dashboard_folder_test.go +++ b/pkg/services/sqlstore/dashboard_folder_test.go @@ -7,21 +7,22 @@ import ( . "github.com/smartystreets/goconvey/convey" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/search" ) func TestDashboardFolderDataAccess(t *testing.T) { Convey("Testing DB", t, func() { - InitTestDB(t) + sqlStore := InitTestDB(t) Convey("Given one dashboard folder with two dashboards and one dashboard in the root folder", func() { - folder := insertTestDashboard(t, "1 test dash folder", 1, 0, true, "prod", "webapp") - dashInRoot := insertTestDashboard(t, "test dash 67", 1, 0, false, "prod", "webapp") - childDash := insertTestDashboard(t, "test dash 23", 1, folder.Id, false, "prod", "webapp") - insertTestDashboard(t, "test dash 45", 1, folder.Id, false, "prod") + folder := insertTestDashboard(t, sqlStore, "1 test dash folder", 1, 0, true, "prod", "webapp") + dashInRoot := insertTestDashboard(t, sqlStore, "test dash 67", 1, 0, false, "prod", "webapp") + childDash := insertTestDashboard(t, sqlStore, "test dash 23", 1, folder.Id, false, "prod", "webapp") + insertTestDashboard(t, sqlStore, "test dash 45", 1, folder.Id, false, "prod") - currentUser := createUser(t, "viewer", "Viewer", false) + currentUser := createUser(t, sqlStore, "viewer", "Viewer", false) Convey("and no acls are set", func() { Convey("should return all dashboards", func() { @@ -40,7 +41,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { Convey("and acl is set for dashboard folder", func() { var otherUser int64 = 999 - err := testHelperUpdateDashboardAcl(folder.Id, models.DashboardAcl{ + err := testHelperUpdateDashboardAcl(t, sqlStore, folder.Id, models.DashboardAcl{ DashboardID: folder.Id, OrgID: 1, UserID: otherUser, @@ -61,7 +62,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("when the user is given permission", func() { - err := testHelperUpdateDashboardAcl(folder.Id, models.DashboardAcl{ + err := testHelperUpdateDashboardAcl(t, sqlStore, folder.Id, models.DashboardAcl{ DashboardID: folder.Id, OrgID: 1, UserID: currentUser.Id, Permission: models.PERMISSION_EDIT, }) So(err, ShouldBeNil) @@ -102,9 +103,9 @@ func TestDashboardFolderDataAccess(t *testing.T) { Convey("and acl is set for dashboard child and folder has all permissions removed", func() { var otherUser int64 = 999 - err := testHelperUpdateDashboardAcl(folder.Id) + err := testHelperUpdateDashboardAcl(t, sqlStore, folder.Id) So(err, ShouldBeNil) - err = testHelperUpdateDashboardAcl(childDash.Id, models.DashboardAcl{ + err = testHelperUpdateDashboardAcl(t, sqlStore, childDash.Id, models.DashboardAcl{ DashboardID: folder.Id, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, }) So(err, ShouldBeNil) @@ -120,7 +121,9 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("when the user is given permission to child", func() { - err := testHelperUpdateDashboardAcl(childDash.Id, models.DashboardAcl{DashboardID: childDash.Id, OrgID: 1, UserID: currentUser.Id, Permission: models.PERMISSION_EDIT}) + err := testHelperUpdateDashboardAcl(t, sqlStore, childDash.Id, models.DashboardAcl{ + DashboardID: childDash.Id, OrgID: 1, UserID: currentUser.Id, Permission: models.PERMISSION_EDIT, + }) So(err, ShouldBeNil) Convey("should be able to search for child dashboard but not folder", func() { @@ -156,18 +159,24 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("Given two dashboard folders with one dashboard each and one dashboard in the root folder", func() { - folder1 := insertTestDashboard(t, "1 test dash folder", 1, 0, true, "prod") - folder2 := insertTestDashboard(t, "2 test dash folder", 1, 0, true, "prod") - dashInRoot := insertTestDashboard(t, "test dash 67", 1, 0, false, "prod") - childDash1 := insertTestDashboard(t, "child dash 1", 1, folder1.Id, false, "prod") - childDash2 := insertTestDashboard(t, "child dash 2", 1, folder2.Id, false, "prod") + folder1 := insertTestDashboard(t, sqlStore, "1 test dash folder", 1, 0, true, "prod") + folder2 := insertTestDashboard(t, sqlStore, "2 test dash folder", 1, 0, true, "prod") + dashInRoot := insertTestDashboard(t, sqlStore, "test dash 67", 1, 0, false, "prod") + childDash1 := insertTestDashboard(t, sqlStore, "child dash 1", 1, folder1.Id, false, "prod") + childDash2 := insertTestDashboard(t, sqlStore, "child dash 2", 1, folder2.Id, false, "prod") - currentUser := createUser(t, "viewer", "Viewer", false) + currentUser := createUser(t, sqlStore, "viewer", "Viewer", false) var rootFolderId int64 = 0 Convey("and one folder is expanded, the other collapsed", func() { Convey("should return dashboards in root and expanded folder", func() { - query := &search.FindPersistedDashboardsQuery{FolderIds: []int64{rootFolderId, folder1.Id}, SignedInUser: &models.SignedInUser{UserId: currentUser.Id, OrgId: 1, OrgRole: models.ROLE_VIEWER}, OrgId: 1} + query := &search.FindPersistedDashboardsQuery{ + FolderIds: []int64{ + rootFolderId, folder1.Id}, SignedInUser: &models.SignedInUser{UserId: currentUser.Id, + OrgId: 1, OrgRole: models.ROLE_VIEWER, + }, + OrgId: 1, + } err := SearchDashboards(query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 4) @@ -179,14 +188,14 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("and acl is set for one dashboard folder", func() { - var otherUser int64 = 999 - err := testHelperUpdateDashboardAcl(folder1.Id, models.DashboardAcl{ + const otherUser int64 = 999 + err := testHelperUpdateDashboardAcl(t, sqlStore, folder1.Id, models.DashboardAcl{ DashboardID: folder1.Id, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, }) So(err, ShouldBeNil) Convey("and a dashboard is moved from folder without acl to the folder with an acl", func() { - moveDashboard(1, childDash2.Data, folder1.Id) + moveDashboard(t, sqlStore, 1, childDash2.Data, folder1.Id) Convey("should not return folder with acl or its children", func() { query := &search.FindPersistedDashboardsQuery{ @@ -201,7 +210,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) }) Convey("and a dashboard is moved from folder with acl to the folder without an acl", func() { - moveDashboard(1, childDash1.Data, folder2.Id) + moveDashboard(t, sqlStore, 1, childDash1.Data, folder2.Id) Convey("should return folder without acl and its children", func() { query := &search.FindPersistedDashboardsQuery{ @@ -220,12 +229,12 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("and a dashboard with an acl is moved to the folder without an acl", func() { - err := testHelperUpdateDashboardAcl(childDash1.Id, models.DashboardAcl{ + err := testHelperUpdateDashboardAcl(t, sqlStore, childDash1.Id, models.DashboardAcl{ DashboardID: childDash1.Id, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, }) So(err, ShouldBeNil) - moveDashboard(1, childDash1.Data, folder2.Id) + moveDashboard(t, sqlStore, 1, childDash1.Data, folder2.Id) Convey("should return folder without acl but not the dashboard with acl", func() { query := &search.FindPersistedDashboardsQuery{ @@ -246,13 +255,13 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("Given two dashboard folders", func() { - folder1 := insertTestDashboard(t, "1 test dash folder", 1, 0, true, "prod") - folder2 := insertTestDashboard(t, "2 test dash folder", 1, 0, true, "prod") - insertTestDashboard(t, "folder in another org", 2, 0, true, "prod") + folder1 := insertTestDashboard(t, sqlStore, "1 test dash folder", 1, 0, true, "prod") + folder2 := insertTestDashboard(t, sqlStore, "2 test dash folder", 1, 0, true, "prod") + insertTestDashboard(t, sqlStore, "folder in another org", 2, 0, true, "prod") - adminUser := createUser(t, "admin", "Admin", true) - editorUser := createUser(t, "editor", "Editor", false) - viewerUser := createUser(t, "viewer", "Viewer", false) + adminUser := createUser(t, sqlStore, "admin", "Admin", true) + editorUser := createUser(t, sqlStore, "editor", "Editor", false) + viewerUser := createUser(t, sqlStore, "viewer", "Viewer", false) Convey("Admin users", func() { Convey("Should have write access to all dashboard folders in their org", func() { @@ -343,7 +352,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("Should have write access to one dashboard folder if default role changed to view for one folder", func() { - err := testHelperUpdateDashboardAcl(folder1.Id, models.DashboardAcl{ + err := testHelperUpdateDashboardAcl(t, sqlStore, folder1.Id, models.DashboardAcl{ DashboardID: folder1.Id, OrgID: 1, UserID: editorUser.Id, Permission: models.PERMISSION_VIEW, }) So(err, ShouldBeNil) @@ -407,7 +416,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("Should be able to get one dashboard folder if default role changed to edit for one folder", func() { - err := testHelperUpdateDashboardAcl(folder1.Id, models.DashboardAcl{ + err := testHelperUpdateDashboardAcl(t, sqlStore, folder1.Id, models.DashboardAcl{ DashboardID: folder1.Id, OrgID: 1, UserID: viewerUser.Id, Permission: models.PERMISSION_EDIT, }) So(err, ShouldBeNil) @@ -438,7 +447,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("and admin permission is given for user with org role viewer in one dashboard folder", func() { - err := testHelperUpdateDashboardAcl(folder1.Id, models.DashboardAcl{ + err := testHelperUpdateDashboardAcl(t, sqlStore, folder1.Id, models.DashboardAcl{ DashboardID: folder1.Id, OrgID: 1, UserID: viewerUser.Id, Permission: models.PERMISSION_ADMIN, }) So(err, ShouldBeNil) @@ -454,7 +463,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("and edit permission is given for user with org role viewer in one dashboard folder", func() { - err := testHelperUpdateDashboardAcl(folder1.Id, models.DashboardAcl{ + err := testHelperUpdateDashboardAcl(t, sqlStore, folder1.Id, models.DashboardAcl{ DashboardID: folder1.Id, OrgID: 1, UserID: viewerUser.Id, Permission: models.PERMISSION_EDIT, }) So(err, ShouldBeNil) @@ -472,3 +481,19 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) }) } + +func moveDashboard(t *testing.T, sqlStore *SQLStore, orgId int64, dashboard *simplejson.Json, + newFolderId int64) *models.Dashboard { + t.Helper() + + cmd := models.SaveDashboardCommand{ + OrgId: orgId, + FolderId: newFolderId, + Dashboard: dashboard, + Overwrite: true, + } + dash, err := sqlStore.SaveDashboard(cmd) + So(err, ShouldBeNil) + + return dash +} diff --git a/pkg/services/sqlstore/dashboard_provisioning.go b/pkg/services/sqlstore/dashboard_provisioning.go index 6c6f81154ae..f993d431838 100644 --- a/pkg/services/sqlstore/dashboard_provisioning.go +++ b/pkg/services/sqlstore/dashboard_provisioning.go @@ -1,6 +1,7 @@ package sqlstore import ( + "context" "errors" "github.com/grafana/grafana/pkg/bus" @@ -8,9 +9,6 @@ import ( ) func init() { - bus.AddHandler("sql", GetProvisionedDashboardDataQuery) - bus.AddHandler("sql", SaveProvisionedDashboard) - bus.AddHandler("sql", GetProvisionedDataByDashboardId) bus.AddHandler("sql", UnprovisionDashboard) bus.AddHandler("sql", DeleteOrphanedProvisionedDashboards) } @@ -22,64 +20,62 @@ type DashboardExtras struct { Value string } -func GetProvisionedDataByDashboardId(cmd *models.GetProvisionedDashboardDataByIdQuery) error { - result := &models.DashboardProvisioning{} - - exist, err := x.Where("dashboard_id = ?", cmd.DashboardId).Get(result) +func (ss *SQLStore) GetProvisionedDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) { + var data models.DashboardProvisioning + exists, err := x.Where("dashboard_id = ?", dashboardID).Get(&data) if err != nil { - return err + return nil, err } - if exist { - cmd.Result = result + if exists { + return &data, nil } - return nil + return nil, nil } -func SaveProvisionedDashboard(cmd *models.SaveProvisionedDashboardCommand) error { - return inTransaction(func(sess *DBSession) error { - err := saveDashboard(sess, cmd.DashboardCmd) - if err != nil { +func (ss *SQLStore) SaveProvisionedDashboard(cmd models.SaveDashboardCommand, + provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { + err := ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error { + if err := saveDashboard(sess, &cmd); err != nil { return err } - cmd.Result = cmd.DashboardCmd.Result - if cmd.DashboardProvisioning.Updated == 0 { - cmd.DashboardProvisioning.Updated = cmd.Result.Updated.Unix() + if provisioning.Updated == 0 { + provisioning.Updated = cmd.Result.Updated.Unix() } - return saveProvisionedData(sess, cmd.DashboardProvisioning, cmd.Result) + return saveProvisionedData(sess, provisioning, cmd.Result) }) + + return cmd.Result, err } -func saveProvisionedData(sess *DBSession, cmd *models.DashboardProvisioning, dashboard *models.Dashboard) error { +func saveProvisionedData(sess *DBSession, provisioning *models.DashboardProvisioning, dashboard *models.Dashboard) error { result := &models.DashboardProvisioning{} - exist, err := sess.Where("dashboard_id=? AND name = ?", dashboard.Id, cmd.Name).Get(result) + exist, err := sess.Where("dashboard_id=? AND name = ?", dashboard.Id, provisioning.Name).Get(result) if err != nil { return err } - cmd.Id = result.Id - cmd.DashboardId = dashboard.Id + provisioning.Id = result.Id + provisioning.DashboardId = dashboard.Id if exist { - _, err = sess.ID(result.Id).Update(cmd) + _, err = sess.ID(result.Id).Update(provisioning) } else { - _, err = sess.Insert(cmd) + _, err = sess.Insert(provisioning) } return err } -func GetProvisionedDashboardDataQuery(cmd *models.GetProvisionedDashboardDataQuery) error { +func (ss *SQLStore) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) { var result []*models.DashboardProvisioning - - if err := x.Where("name = ?", cmd.Name).Find(&result); err != nil { - return err + if err := ss.engine.Where("name = ?", name).Find(&result); err != nil { + return nil, err } - cmd.Result = result - return nil + return result, nil } // UnprovisionDashboard removes row in dashboard_provisioning for the dashboard making it seem as if manually created. diff --git a/pkg/services/sqlstore/dashboard_provisioning_test.go b/pkg/services/sqlstore/dashboard_provisioning_test.go index 1283f72bec4..192102c23a4 100644 --- a/pkg/services/sqlstore/dashboard_provisioning_test.go +++ b/pkg/services/sqlstore/dashboard_provisioning_test.go @@ -13,9 +13,9 @@ import ( func TestDashboardProvisioningTest(t *testing.T) { Convey("Testing Dashboard provisioning", t, func() { - InitTestDB(t) + sqlStore := InitTestDB(t) - folderCmd := &models.SaveDashboardCommand{ + folderCmd := models.SaveDashboardCommand{ OrgId: 1, FolderId: 0, IsFolder: true, @@ -25,13 +25,13 @@ func TestDashboardProvisioningTest(t *testing.T) { }), } - err := SaveDashboard(folderCmd) + dash, err := sqlStore.SaveDashboard(folderCmd) So(err, ShouldBeNil) - saveDashboardCmd := &models.SaveDashboardCommand{ + saveDashboardCmd := models.SaveDashboardCommand{ OrgId: 1, IsFolder: false, - FolderId: folderCmd.Result.Id, + FolderId: dash.Id, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": "test dashboard", @@ -41,43 +41,38 @@ func TestDashboardProvisioningTest(t *testing.T) { Convey("Saving dashboards with provisioning meta data", func() { now := time.Now() - cmd := &models.SaveProvisionedDashboardCommand{ - DashboardCmd: saveDashboardCmd, - DashboardProvisioning: &models.DashboardProvisioning{ - Name: "default", - ExternalId: "/var/grafana.json", - Updated: now.Unix(), - }, + provisioning := &models.DashboardProvisioning{ + Name: "default", + ExternalId: "/var/grafana.json", + Updated: now.Unix(), } - err := SaveProvisionedDashboard(cmd) + dash, err := sqlStore.SaveProvisionedDashboard(saveDashboardCmd, provisioning) So(err, ShouldBeNil) - So(cmd.Result, ShouldNotBeNil) - So(cmd.Result.Id, ShouldNotEqual, 0) - dashId := cmd.Result.Id + So(dash, ShouldNotBeNil) + So(dash.Id, ShouldNotEqual, 0) + dashId := dash.Id Convey("Deleting orphaned provisioned dashboards", func() { - anotherCmd := &models.SaveProvisionedDashboardCommand{ - DashboardCmd: &models.SaveDashboardCommand{ - OrgId: 1, - IsFolder: false, - FolderId: folderCmd.Result.Id, - Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": nil, - "title": "another_dashboard", - }), - }, - DashboardProvisioning: &models.DashboardProvisioning{ - Name: "another_reader", - ExternalId: "/var/grafana.json", - Updated: now.Unix(), - }, + saveCmd := models.SaveDashboardCommand{ + OrgId: 1, + IsFolder: false, + FolderId: dash.Id, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": "another_dashboard", + }), + } + provisioning := &models.DashboardProvisioning{ + Name: "another_reader", + ExternalId: "/var/grafana.json", + Updated: now.Unix(), } - err := SaveProvisionedDashboard(anotherCmd) + anotherDash, err := sqlStore.SaveProvisionedDashboard(saveCmd, provisioning) So(err, ShouldBeNil) - query := &models.GetDashboardsQuery{DashboardIds: []int64{anotherCmd.Result.Id}} + query := &models.GetDashboardsQuery{DashboardIds: []int64{anotherDash.Id}} err = GetDashboards(query) So(err, ShouldBeNil) So(query.Result, ShouldNotBeNil) @@ -85,7 +80,7 @@ func TestDashboardProvisioningTest(t *testing.T) { deleteCmd := &models.DeleteOrphanedProvisionedDashboardsCommand{ReaderNames: []string{"default"}} So(DeleteOrphanedProvisionedDashboards(deleteCmd), ShouldBeNil) - query = &models.GetDashboardsQuery{DashboardIds: []int64{cmd.Result.Id, anotherCmd.Result.Id}} + query = &models.GetDashboardsQuery{DashboardIds: []int64{dash.Id, anotherDash.Id}} err = GetDashboards(query) So(err, ShouldBeNil) @@ -94,45 +89,38 @@ func TestDashboardProvisioningTest(t *testing.T) { }) Convey("Can query for provisioned dashboards", func() { - query := &models.GetProvisionedDashboardDataQuery{Name: "default"} - err := GetProvisionedDashboardDataQuery(query) + rslt, err := sqlStore.GetProvisionedDashboardData("default") So(err, ShouldBeNil) - So(len(query.Result), ShouldEqual, 1) - So(query.Result[0].DashboardId, ShouldEqual, dashId) - So(query.Result[0].Updated, ShouldEqual, now.Unix()) + So(len(rslt), ShouldEqual, 1) + So(rslt[0].DashboardId, ShouldEqual, dashId) + So(rslt[0].Updated, ShouldEqual, now.Unix()) }) Convey("Can query for one provisioned dashboard", func() { - query := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: cmd.Result.Id} - - err := GetProvisionedDataByDashboardId(query) + data, err := sqlStore.GetProvisionedDataByDashboardID(dash.Id) So(err, ShouldBeNil) - So(query.Result, ShouldNotBeNil) + So(data, ShouldNotBeNil) }) Convey("Can query for none provisioned dashboard", func() { - query := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: 3000} - - err := GetProvisionedDataByDashboardId(query) + data, err := sqlStore.GetProvisionedDataByDashboardID(3000) So(err, ShouldBeNil) - So(query.Result, ShouldBeNil) + So(data, ShouldBeNil) }) Convey("Deleting folder should delete provision meta data", func() { deleteCmd := &models.DeleteDashboardCommand{ - Id: folderCmd.Result.Id, + Id: dash.Id, OrgId: 1, } So(DeleteDashboard(deleteCmd), ShouldBeNil) - query := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: cmd.Result.Id} - - err = GetProvisionedDataByDashboardId(query) + data, err := sqlStore.GetProvisionedDataByDashboardID(dash.Id) So(err, ShouldBeNil) - So(query.Result, ShouldBeNil) + So(data, ShouldBeNil) }) Convey("UnprovisionDashboard should delete provisioning metadata", func() { @@ -142,11 +130,9 @@ func TestDashboardProvisioningTest(t *testing.T) { So(UnprovisionDashboard(unprovisionCmd), ShouldBeNil) - query := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: dashId} - - err = GetProvisionedDataByDashboardId(query) + data, err := sqlStore.GetProvisionedDataByDashboardID(dashId) So(err, ShouldBeNil) - So(query.Result, ShouldBeNil) + So(data, ShouldBeNil) }) }) }) diff --git a/pkg/services/sqlstore/dashboard_service_integration_test.go b/pkg/services/sqlstore/dashboard_service_integration_test.go deleted file mode 100644 index 54716a424cb..00000000000 --- a/pkg/services/sqlstore/dashboard_service_integration_test.go +++ /dev/null @@ -1,1047 +0,0 @@ -// +build integration - -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/stretchr/testify/require" - - "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 - }) - - bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error { - cmd.Result = nil - return nil - }) - - savedFolder := saveTestFolder(t, "Saved folder", testOrgId) - savedDashInFolder := saveTestDashboard(t, "Saved dash in folder", testOrgId, savedFolder.Id) - saveTestDashboard(t, "Other saved dash in folder", testOrgId, savedFolder.Id) - savedDashInGeneralFolder := saveTestDashboard(t, "Saved dashboard in general folder", testOrgId, 0) - otherSavedFolder := saveTestFolder(t, "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 creating a dashboard with same id as dashboard 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 creating a dashboard with same uid as dashboard 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(t, cmd) - - Convey("It should create a new dashboard in organization B", 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 creating 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 create dashboard guardian for General Folder 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 creating 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 create dashboard guardian for other folder 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 creating a new dashboard by existing title in folder", func() { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgId, - Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "title": savedDashInFolder.Title, - }), - FolderId: savedFolder.Id, - UserId: 10000, - Overwrite: true, - } - - err := callSaveWithError(cmd) - - Convey("It should create dashboard guardian for folder with correct arguments and result in access denied error", func() { - So(err, ShouldNotBeNil) - So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied) - - So(sc.dashboardGuardianMock.DashId, ShouldEqual, savedFolder.Id) - So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId) - So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId) - }) - }) - - Convey("When creating a new dashboard by existing uid in folder", func() { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgId, - Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "uid": savedDashInFolder.Uid, - "title": "New dash", - }), - FolderId: savedFolder.Id, - UserId: 10000, - Overwrite: true, - } - - err := callSaveWithError(cmd) - - Convey("It should create dashboard guardian for folder with correct arguments and result in access denied error", func() { - So(err, ShouldNotBeNil) - So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied) - - So(sc.dashboardGuardianMock.DashId, ShouldEqual, savedFolder.Id) - So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId) - So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId) - }) - }) - - Convey("When updating 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 create dashboard guardian for dashboard 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 updating 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 create dashboard guardian for dashboard 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) - }) - }) - - Convey("When moving a dashboard by existing id to other folder from General folder", func() { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgId, - Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": savedDashInGeneralFolder.Id, - "title": "Dash", - }), - FolderId: otherSavedFolder.Id, - UserId: 10000, - Overwrite: true, - } - - err := callSaveWithError(cmd) - - Convey("It should create dashboard guardian for other folder with correct arguments and result 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 moving a dashboard by existing id to the General folder from other folder", func() { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgId, - Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": savedDashInFolder.Id, - "title": "Dash", - }), - FolderId: 0, - UserId: 10000, - Overwrite: true, - } - - err := callSaveWithError(cmd) - - Convey("It should create dashboard guardian for General folder 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 moving a dashboard by existing uid to other folder from General folder", func() { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgId, - Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "uid": savedDashInGeneralFolder.Uid, - "title": "Dash", - }), - FolderId: otherSavedFolder.Id, - UserId: 10000, - Overwrite: true, - } - - err := callSaveWithError(cmd) - - Convey("It should create dashboard guardian for other folder with correct arguments and result 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 moving a dashboard by existing uid to the General folder from other folder", func() { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgId, - Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "uid": savedDashInFolder.Uid, - "title": "Dash", - }), - FolderId: 0, - UserId: 10000, - Overwrite: true, - } - - err := callSaveWithError(cmd) - - Convey("It should create dashboard guardian for General folder 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) - }) - }) - }) - - // 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(t, 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(t, 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(t, 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(t, 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(t, 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.ErrDashboardFolderNotFound) - }) - }) - - 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(t, 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(t, 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(t, 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(t, 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(t, 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(t, 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(t, 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 updating 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 updating 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 updating 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 updating 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 updating 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 updating 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) - }) - }) - }) - }) - }) - }) -} - -type dashboardPermissionScenarioContext struct { - dashboardGuardianMock *guardian.FakeDashboardGuardian -} - -type dashboardPermissionScenarioFunc func(sc *dashboardPermissionScenarioContext) - -func dashboardPermissionScenario(desc string, mock *guardian.FakeDashboardGuardian, fn dashboardPermissionScenarioFunc) { - Convey(desc, func() { - origNewDashboardGuardian := guardian.New - guardian.MockDashboardGuardian(mock) - - sc := &dashboardPermissionScenarioContext{ - dashboardGuardianMock: mock, - } - - defer func() { - guardian.New = origNewDashboardGuardian - }() - - fn(sc) - }) -} - -func permissionScenario(desc string, canSave bool, fn dashboardPermissionScenarioFunc) { - mock := &guardian.FakeDashboardGuardian{ - CanSaveValue: canSave, - } - dashboardPermissionScenario(desc, mock, fn) -} - -func callSaveWithResult(t *testing.T, cmd models.SaveDashboardCommand) *models.Dashboard { - t.Helper() - - dto := toSaveDashboardDto(cmd) - res, err := dashboards.NewService().SaveDashboard(&dto, false) - require.NoError(t, err) - - return res -} - -func callSaveWithError(cmd models.SaveDashboardCommand) error { - dto := toSaveDashboardDto(cmd) - _, err := dashboards.NewService().SaveDashboard(&dto, false) - return err -} - -func saveTestDashboard(t *testing.T, title string, orgId int64, folderId int64) *models.Dashboard { - t.Helper() - - 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, false) - require.NoError(t, err) - - return res -} - -func saveTestFolder(t *testing.T, title string, orgId int64) *models.Dashboard { - t.Helper() - 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, false) - require.NoError(t, err) - - 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 21cf0ab4fe0..7ace445004e 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -22,13 +22,13 @@ import ( func TestDashboardDataAccess(t *testing.T) { Convey("Testing DB", t, func() { - InitTestDB(t) + sqlStore := InitTestDB(t) Convey("Given saved dashboard", func() { - savedFolder := insertTestDashboard(t, "1 test dash folder", 1, 0, true, "prod", "webapp") - savedDash := insertTestDashboard(t, "test dash 23", 1, savedFolder.Id, false, "prod", "webapp") - insertTestDashboard(t, "test dash 45", 1, savedFolder.Id, false, "prod") - insertTestDashboard(t, "test dash 67", 1, 0, false, "prod", "webapp") + savedFolder := insertTestDashboard(t, sqlStore, "1 test dash folder", 1, 0, true, "prod", "webapp") + savedDash := insertTestDashboard(t, sqlStore, "test dash 23", 1, savedFolder.Id, false, "prod", "webapp") + insertTestDashboard(t, sqlStore, "test dash 45", 1, savedFolder.Id, false, "prod") + insertTestDashboard(t, sqlStore, "test dash 67", 1, 0, false, "prod", "webapp") Convey("Should return dashboard model", func() { So(savedDash.Title, ShouldEqual, "test dash 23") @@ -104,7 +104,7 @@ func TestDashboardDataAccess(t *testing.T) { }) Convey("Should be able to delete dashboard", func() { - dash := insertTestDashboard(t, "delete me", 1, 0, false, "delete this") + dash := insertTestDashboard(t, sqlStore, "delete me", 1, 0, false, "delete this") err := DeleteDashboard(&models.DeleteDashboardCommand{ Id: dash.Id, @@ -129,8 +129,7 @@ func TestDashboardDataAccess(t *testing.T) { "tags": []interface{}{}, }), } - - err := SaveDashboard(&cmd) + _, err := sqlStore.SaveDashboard(cmd) So(err, ShouldBeNil) generateNewUid = util.GenerateShortUID @@ -145,13 +144,12 @@ func TestDashboardDataAccess(t *testing.T) { }), UserId: 100, } - - err := SaveDashboard(&cmd) + dashboard, err := sqlStore.SaveDashboard(cmd) So(err, ShouldBeNil) - So(cmd.Result.CreatedBy, ShouldEqual, 100) - So(cmd.Result.Created.IsZero(), ShouldBeFalse) - So(cmd.Result.UpdatedBy, ShouldEqual, 100) - So(cmd.Result.Updated.IsZero(), ShouldBeFalse) + So(dashboard.CreatedBy, ShouldEqual, 100) + So(dashboard.Created.IsZero(), ShouldBeFalse) + So(dashboard.UpdatedBy, ShouldEqual, 100) + So(dashboard.Updated.IsZero(), ShouldBeFalse) }) Convey("Should be able to update dashboard by id and remove folderId", func() { @@ -166,10 +164,9 @@ func TestDashboardDataAccess(t *testing.T) { FolderId: 2, UserId: 100, } - - err := SaveDashboard(&cmd) + dash, err := sqlStore.SaveDashboard(cmd) So(err, ShouldBeNil) - So(cmd.Result.FolderId, ShouldEqual, 2) + So(dash.FolderId, ShouldEqual, 2) cmd = models.SaveDashboardCommand{ OrgId: 1, @@ -182,8 +179,7 @@ func TestDashboardDataAccess(t *testing.T) { Overwrite: true, UserId: 100, } - - err = SaveDashboard(&cmd) + _, err = sqlStore.SaveDashboard(cmd) So(err, ShouldBeNil) query := models.GetDashboardQuery{ @@ -201,7 +197,7 @@ func TestDashboardDataAccess(t *testing.T) { }) Convey("Should be able to delete empty folder", func() { - emptyFolder := insertTestDashboard(t, "2 test dash folder", 1, 0, true, "prod", "webapp") + emptyFolder := insertTestDashboard(t, sqlStore, "2 test dash folder", 1, 0, true, "prod", "webapp") deleteCmd := &models.DeleteDashboardCommand{Id: emptyFolder.Id} err := DeleteDashboard(deleteCmd) @@ -236,7 +232,7 @@ func TestDashboardDataAccess(t *testing.T) { }), } - err := SaveDashboard(&cmd) + _, err := sqlStore.SaveDashboard(cmd) So(err, ShouldEqual, models.ErrDashboardNotFound) }) @@ -250,8 +246,7 @@ func TestDashboardDataAccess(t *testing.T) { "tags": []interface{}{}, }), } - - err := SaveDashboard(&cmd) + _, err := sqlStore.SaveDashboard(cmd) So(err, ShouldBeNil) }) @@ -366,7 +361,7 @@ func TestDashboardDataAccess(t *testing.T) { }) Convey("Given two dashboards, one is starred dashboard by user 10, other starred by user 1", func() { - starredDash := insertTestDashboard(t, "starred dash", 1, 0, false) + starredDash := insertTestDashboard(t, sqlStore, "starred dash", 1, 0, false) err := StarDashboard(&models.StarDashboardCommand{ DashboardId: starredDash.Id, UserId: 10, @@ -396,9 +391,9 @@ func TestDashboardDataAccess(t *testing.T) { Convey("Given a plugin with imported dashboards", func() { pluginId := "test-app" - appFolder := insertTestDashboardForPlugin("app-test", 1, 0, true, pluginId) - insertTestDashboardForPlugin("app-dash1", 1, appFolder.Id, false, pluginId) - insertTestDashboardForPlugin("app-dash2", 1, appFolder.Id, false, pluginId) + appFolder := insertTestDashboardForPlugin(t, sqlStore, "app-test", 1, 0, true, pluginId) + insertTestDashboardForPlugin(t, sqlStore, "app-dash1", 1, appFolder.Id, false, pluginId) + insertTestDashboardForPlugin(t, sqlStore, "app-dash2", 1, appFolder.Id, false, pluginId) Convey("Should return imported dashboard", func() { query := models.GetDashboardsByPluginIdQuery{ @@ -417,9 +412,9 @@ func TestDashboardDataAccess(t *testing.T) { func TestDashboard_SortingOptions(t *testing.T) { // insertTestDashboard uses GoConvey's assertions. Workaround. Convey("test with multiple sorting options", t, func() { - InitTestDB(t) - dashB := insertTestDashboard(t, "Beta", 1, 0, false) - dashA := insertTestDashboard(t, "Alfa", 1, 0, false) + sqlStore := InitTestDB(t) + dashB := insertTestDashboard(t, sqlStore, "Beta", 1, 0, false) + dashA := insertTestDashboard(t, sqlStore, "Alfa", 1, 0, false) assert.NotZero(t, dashA.Id) assert.Less(t, dashB.Id, dashA.Id) @@ -441,7 +436,8 @@ func TestDashboard_SortingOptions(t *testing.T) { }) } -func insertTestDashboard(t *testing.T, title string, orgId int64, folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard { +func insertTestDashboard(t *testing.T, sqlStore *SQLStore, title string, orgId int64, + folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard { t.Helper() cmd := models.SaveDashboardCommand{ @@ -454,17 +450,20 @@ func insertTestDashboard(t *testing.T, title string, orgId int64, folderId int64 "tags": tags, }), } - - err := SaveDashboard(&cmd) + dash, err := sqlStore.SaveDashboard(cmd) require.NoError(t, err) + require.NotNil(t, dash) - cmd.Result.Data.Set("id", cmd.Result.Id) - cmd.Result.Data.Set("uid", cmd.Result.Uid) + dash.Data.Set("id", dash.Id) + dash.Data.Set("uid", dash.Uid) - return cmd.Result + return dash } -func insertTestDashboardForPlugin(title string, orgId int64, folderId int64, isFolder bool, pluginId string) *models.Dashboard { +func insertTestDashboardForPlugin(t *testing.T, sqlStore *SQLStore, title string, orgId int64, + folderId int64, isFolder bool, pluginId string) *models.Dashboard { + t.Helper() + cmd := models.SaveDashboardCommand{ OrgId: orgId, FolderId: folderId, @@ -476,13 +475,13 @@ func insertTestDashboardForPlugin(title string, orgId int64, folderId int64, isF PluginId: pluginId, } - err := SaveDashboard(&cmd) + dash, err := sqlStore.SaveDashboard(cmd) So(err, ShouldBeNil) - return cmd.Result + return dash } -func createUser(t *testing.T, name string, role string, isAdmin bool) models.User { +func createUser(t *testing.T, sqlStore *SQLStore, name string, role string, isAdmin bool) models.User { t.Helper() setting.AutoAssignOrg = true @@ -490,27 +489,13 @@ func createUser(t *testing.T, name string, role string, isAdmin bool) models.Use setting.AutoAssignOrgRole = role currentUserCmd := models.CreateUserCommand{Login: name, Email: name + "@test.com", Name: "a " + name, IsAdmin: isAdmin} - err := CreateUser(context.Background(), ¤tUserCmd) + currentUser, err := sqlStore.CreateUser(context.Background(), currentUserCmd) require.NoError(t, err) - q1 := models.GetUserOrgListQuery{UserId: currentUserCmd.Result.Id} + q1 := models.GetUserOrgListQuery{UserId: currentUser.Id} err = GetUserOrgList(&q1) require.NoError(t, err) require.Equal(t, models.RoleType(role), q1.Result[0].Role) - return currentUserCmd.Result -} - -func moveDashboard(orgId int64, dashboard *simplejson.Json, newFolderId int64) *models.Dashboard { - cmd := models.SaveDashboardCommand{ - OrgId: orgId, - FolderId: newFolderId, - Dashboard: dashboard, - Overwrite: true, - } - - err := SaveDashboard(&cmd) - So(err, ShouldBeNil) - - return cmd.Result + return *currentUser } diff --git a/pkg/services/sqlstore/dashboard_version_test.go b/pkg/services/sqlstore/dashboard_version_test.go index b7df727c799..20064d33929 100644 --- a/pkg/services/sqlstore/dashboard_version_test.go +++ b/pkg/services/sqlstore/dashboard_version_test.go @@ -13,7 +13,9 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -func updateTestDashboard(dashboard *models.Dashboard, data map[string]interface{}) { +func updateTestDashboard(t *testing.T, sqlStore *SQLStore, dashboard *models.Dashboard, data map[string]interface{}) { + t.Helper() + data["id"] = dashboard.Id saveCmd := models.SaveDashboardCommand{ @@ -21,17 +23,16 @@ func updateTestDashboard(dashboard *models.Dashboard, data map[string]interface{ Overwrite: true, Dashboard: simplejson.NewFromAny(data), } - - err := SaveDashboard(&saveCmd) + _, err := sqlStore.SaveDashboard(saveCmd) So(err, ShouldBeNil) } func TestGetDashboardVersion(t *testing.T) { Convey("Testing dashboard version retrieval", t, func() { - InitTestDB(t) + sqlStore := InitTestDB(t) Convey("Get a Dashboard ID and version ID", func() { - savedDash := insertTestDashboard(t, "test dash 26", 1, 0, false, "diff") + savedDash := insertTestDashboard(t, sqlStore, "test dash 26", 1, 0, false, "diff") query := models.GetDashboardVersionQuery{ DashboardId: savedDash.Id, @@ -71,8 +72,8 @@ func TestGetDashboardVersion(t *testing.T) { func TestGetDashboardVersions(t *testing.T) { Convey("Testing dashboard versions retrieval", t, func() { - InitTestDB(t) - savedDash := insertTestDashboard(t, "test dash 43", 1, 0, false, "diff-all") + sqlStore := InitTestDB(t) + savedDash := insertTestDashboard(t, sqlStore, "test dash 43", 1, 0, false, "diff-all") Convey("Get all versions for a given Dashboard ID", func() { query := models.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1} @@ -92,7 +93,7 @@ func TestGetDashboardVersions(t *testing.T) { }) Convey("Get all versions for an updated dashboard", func() { - updateTestDashboard(savedDash, map[string]interface{}{ + updateTestDashboard(t, sqlStore, savedDash, map[string]interface{}{ "tags": "different-tag", }) @@ -107,14 +108,14 @@ func TestGetDashboardVersions(t *testing.T) { func TestDeleteExpiredVersions(t *testing.T) { Convey("Testing dashboard versions clean up", t, func() { - InitTestDB(t) + sqlStore := InitTestDB(t) versionsToKeep := 5 versionsToWrite := 10 setting.DashboardVersionsToKeep = versionsToKeep - savedDash := insertTestDashboard(t, "test dash 53", 1, 0, false, "diff-all") + savedDash := insertTestDashboard(t, sqlStore, "test dash 53", 1, 0, false, "diff-all") for i := 0; i < versionsToWrite-1; i++ { - updateTestDashboard(savedDash, map[string]interface{}{ + updateTestDashboard(t, sqlStore, savedDash, map[string]interface{}{ "tags": "different-tag", }) } @@ -152,7 +153,7 @@ func TestDeleteExpiredVersions(t *testing.T) { versionsToWriteBigNumber := perBatch*maxBatches + versionsToWrite for i := 0; i < versionsToWriteBigNumber-versionsToWrite; i++ { - updateTestDashboard(savedDash, map[string]interface{}{ + updateTestDashboard(t, sqlStore, savedDash, map[string]interface{}{ "tags": "different-tag", }) } diff --git a/pkg/services/sqlstore/org_test.go b/pkg/services/sqlstore/org_test.go index 73c0990d464..05940c62695 100644 --- a/pkg/services/sqlstore/org_test.go +++ b/pkg/services/sqlstore/org_test.go @@ -15,7 +15,7 @@ import ( func TestAccountDataAccess(t *testing.T) { Convey("Testing Account DB Access", t, func() { - InitTestDB(t) + sqlStore := InitTestDB(t) Convey("Given we have organizations, we can query them by IDs", func() { var err error @@ -78,13 +78,13 @@ func TestAccountDataAccess(t *testing.T) { ac1cmd := models.CreateUserCommand{Login: "ac1", Email: "ac1@test.com", Name: "ac1 name"} ac2cmd := models.CreateUserCommand{Login: "ac2", Email: "ac2@test.com", Name: "ac2 name"} - err := CreateUser(context.Background(), &ac1cmd) + ac1, err := sqlStore.CreateUser(context.Background(), ac1cmd) So(err, ShouldBeNil) - err = CreateUser(context.Background(), &ac2cmd) + ac2, err := sqlStore.CreateUser(context.Background(), ac2cmd) So(err, ShouldBeNil) - q1 := models.GetUserOrgListQuery{UserId: ac1cmd.Result.Id} - q2 := models.GetUserOrgListQuery{UserId: ac2cmd.Result.Id} + q1 := models.GetUserOrgListQuery{UserId: ac1.Id} + q2 := models.GetUserOrgListQuery{UserId: ac2.Id} err = GetUserOrgList(&q1) So(err, ShouldBeNil) err = GetUserOrgList(&q2) @@ -101,13 +101,10 @@ func TestAccountDataAccess(t *testing.T) { ac1cmd := models.CreateUserCommand{Login: "ac1", Email: "ac1@test.com", Name: "ac1 name"} ac2cmd := models.CreateUserCommand{Login: "ac2", Email: "ac2@test.com", Name: "ac2 name", IsAdmin: true} - err := CreateUser(context.Background(), &ac1cmd) - err = CreateUser(context.Background(), &ac2cmd) + ac1, err := sqlStore.CreateUser(context.Background(), ac1cmd) + ac2, err := sqlStore.CreateUser(context.Background(), ac2cmd) So(err, ShouldBeNil) - ac1 := ac1cmd.Result - ac2 := ac2cmd.Result - Convey("Should be able to read user info projection", func() { query := models.GetUserProfileQuery{UserId: ac1.Id} err = GetUserProfile(&query) @@ -266,9 +263,8 @@ func TestAccountDataAccess(t *testing.T) { Convey("Given an org user with dashboard permissions", func() { ac3cmd := models.CreateUserCommand{Login: "ac3", Email: "ac3@test.com", Name: "ac3 name", IsAdmin: false} - err := CreateUser(context.Background(), &ac3cmd) + ac3, err := sqlStore.CreateUser(context.Background(), ac3cmd) So(err, ShouldBeNil) - ac3 := ac3cmd.Result orgUserCmd := models.AddOrgUserCommand{ OrgId: ac1.OrgId, @@ -284,13 +280,17 @@ func TestAccountDataAccess(t *testing.T) { So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 3) - dash1 := insertTestDashboard(t, "1 test dash", ac1.OrgId, 0, false, "prod", "webapp") - dash2 := insertTestDashboard(t, "2 test dash", ac3.OrgId, 0, false, "prod", "webapp") + dash1 := insertTestDashboard(t, sqlStore, "1 test dash", ac1.OrgId, 0, false, "prod", "webapp") + dash2 := insertTestDashboard(t, sqlStore, "2 test dash", ac3.OrgId, 0, false, "prod", "webapp") - err = testHelperUpdateDashboardAcl(dash1.Id, models.DashboardAcl{DashboardID: dash1.Id, OrgID: ac1.OrgId, UserID: ac3.Id, Permission: models.PERMISSION_EDIT}) + err = testHelperUpdateDashboardAcl(t, sqlStore, dash1.Id, models.DashboardAcl{ + DashboardID: dash1.Id, OrgID: ac1.OrgId, UserID: ac3.Id, Permission: models.PERMISSION_EDIT, + }) So(err, ShouldBeNil) - err = testHelperUpdateDashboardAcl(dash2.Id, models.DashboardAcl{DashboardID: dash2.Id, OrgID: ac3.OrgId, UserID: ac3.Id, Permission: models.PERMISSION_EDIT}) + err = testHelperUpdateDashboardAcl(t, sqlStore, dash2.Id, models.DashboardAcl{ + DashboardID: dash2.Id, OrgID: ac3.OrgId, UserID: ac3.Id, Permission: models.PERMISSION_EDIT, + }) So(err, ShouldBeNil) Convey("When org user is deleted", func() { @@ -322,13 +322,16 @@ func TestAccountDataAccess(t *testing.T) { }) } -func testHelperUpdateDashboardAcl(dashboardId int64, items ...models.DashboardAcl) error { - cmd := models.UpdateDashboardAclCommand{DashboardID: dashboardId} - for _, i := range items { - item := i +func testHelperUpdateDashboardAcl(t *testing.T, sqlStore *SQLStore, dashboardID int64, + items ...models.DashboardAcl) error { + t.Helper() + + var itemPtrs []*models.DashboardAcl + for _, it := range items { + item := it item.Created = time.Now() item.Updated = time.Now() - cmd.Items = append(cmd.Items, &item) + itemPtrs = append(itemPtrs, &item) } - return UpdateDashboardAcl(&cmd) + return sqlStore.UpdateDashboardACL(dashboardID, itemPtrs) } diff --git a/pkg/services/sqlstore/plugin_setting.go b/pkg/services/sqlstore/plugin_setting.go index a5ca6a7a0c9..0b010f11a14 100644 --- a/pkg/services/sqlstore/plugin_setting.go +++ b/pkg/services/sqlstore/plugin_setting.go @@ -10,25 +10,27 @@ import ( ) func init() { - bus.AddHandler("sql", GetPluginSettings) bus.AddHandler("sql", GetPluginSettingById) bus.AddHandler("sql", UpdatePluginSetting) bus.AddHandler("sql", UpdatePluginSettingVersion) } -func GetPluginSettings(query *models.GetPluginSettingsQuery) error { +func (ss *SQLStore) GetPluginSettings(orgID int64) ([]*models.PluginSettingInfoDTO, error) { sql := `SELECT org_id, plugin_id, enabled, pinned, plugin_version FROM plugin_setting ` params := make([]interface{}, 0) - if query.OrgId != 0 { + if orgID != 0 { sql += "WHERE org_id=?" - params = append(params, query.OrgId) + params = append(params, orgID) } sess := x.SQL(sql, params...) - query.Result = make([]*models.PluginSettingInfoDTO, 0) - return sess.Find(&query.Result) + var rslt []*models.PluginSettingInfoDTO + if err := sess.Find(&rslt); err != nil { + return nil, err + } + return rslt, nil } func GetPluginSettingById(query *models.GetPluginSettingByIdQuery) error { diff --git a/pkg/services/sqlstore/searchstore/search_test.go b/pkg/services/sqlstore/searchstore/search_test.go index 9a799e40891..12136094633 100644 --- a/pkg/services/sqlstore/searchstore/search_test.go +++ b/pkg/services/sqlstore/searchstore/search_test.go @@ -5,7 +5,6 @@ package searchstore_test import ( "context" - "fmt" "testing" "time" @@ -34,13 +33,11 @@ func TestBuilder_EqualResults_Basic(t *testing.T) { } db := setupTestEnvironment(t) - err := createDashboards(0, 1, user.OrgId) - require.NoError(t, err) + createDashboards(t, db, 0, 1, user.OrgId) // create one dashboard in another organization that shouldn't // be listed in the results. - err = createDashboards(1, 2, 2) - require.NoError(t, err) + createDashboards(t, db, 1, 2, 2) builder := &searchstore.Builder{ Filters: []interface{}{ @@ -51,7 +48,7 @@ func TestBuilder_EqualResults_Basic(t *testing.T) { } res := []sqlstore.DashboardSearchProjection{} - err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + err := db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { sql, params := builder.ToSQL(limit, page) return sess.SQL(sql, params...).Find(&res) }) @@ -77,8 +74,7 @@ func TestBuilder_Pagination(t *testing.T) { } db := setupTestEnvironment(t) - err := createDashboards(0, 25, user.OrgId) - require.NoError(t, err) + createDashboards(t, db, 0, 25, user.OrgId) builder := &searchstore.Builder{ Filters: []interface{}{ @@ -91,7 +87,7 @@ func TestBuilder_Pagination(t *testing.T) { resPg1 := []sqlstore.DashboardSearchProjection{} resPg2 := []sqlstore.DashboardSearchProjection{} resPg3 := []sqlstore.DashboardSearchProjection{} - err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + err := db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { sql, params := builder.ToSQL(15, 1) err := sess.SQL(sql, params...).Find(&resPg1) if err != nil { @@ -124,8 +120,7 @@ func TestBuilder_Permissions(t *testing.T) { } db := setupTestEnvironment(t) - err := createDashboards(0, 1, user.OrgId) - require.NoError(t, err) + createDashboards(t, db, 0, 1, user.OrgId) level := models.PERMISSION_EDIT @@ -145,7 +140,7 @@ func TestBuilder_Permissions(t *testing.T) { } res := []sqlstore.DashboardSearchProjection{} - err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + err := db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { sql, params := builder.ToSQL(limit, page) return sess.SQL(sql, params...).Find(&res) }) @@ -161,10 +156,10 @@ func setupTestEnvironment(t *testing.T) *sqlstore.SQLStore { return store } -func createDashboards(startID, endID int, orgID int64) error { - if endID < startID { - return fmt.Errorf("startID must be smaller than endID") - } +func createDashboards(t *testing.T, db *sqlstore.SQLStore, startID, endID int, orgID int64) { + t.Helper() + + require.GreaterOrEqual(t, endID, startID) for i := startID; i < endID; i++ { dashboard, err := simplejson.NewJson([]byte(`{ @@ -176,20 +171,15 @@ func createDashboards(startID, endID int, orgID int64) error { "schemaVersion": 16, "version": 0 }`)) - if err != nil { - return err - } - err = sqlstore.SaveDashboard(&models.SaveDashboardCommand{ + require.NoError(t, err) + _, err = db.SaveDashboard(models.SaveDashboardCommand{ Dashboard: dashboard, UserId: 1, OrgId: orgID, UpdatedAt: time.Now(), }) - if err != nil { - return err - } + require.NoError(t, err) } - return nil } // lexiCounter counts in a lexicographically sortable order. diff --git a/pkg/services/sqlstore/sqlbuilder_test.go b/pkg/services/sqlstore/sqlbuilder_test.go index 71e0a72eb3d..46ca2d277c5 100644 --- a/pkg/services/sqlstore/sqlbuilder_test.go +++ b/pkg/services/sqlstore/sqlbuilder_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestSQLBuilder(t *testing.T) { @@ -138,8 +139,8 @@ func TestSQLBuilder(t *testing.T) { }) } -var shouldFind = true -var shouldNotFind = false +const shouldFind = true +const shouldNotFind = false type DashboardProps struct { OrgId int64 @@ -164,38 +165,35 @@ type dashboardResponse struct { } func test(t *testing.T, dashboardProps DashboardProps, dashboardPermission *DashboardPermission, search Search, shouldFind bool) { - // Will also cleanup the db - sqlStore := InitTestDB(t) + t.Helper() - dashboard, err := createDummyDashboard(dashboardProps) - if !assert.Equal(t, nil, err) { - return - } + t.Run("", func(t *testing.T) { + // Will also cleanup the db + sqlStore := InitTestDB(t) - var aclUserId int64 - if dashboardPermission != nil { - aclUserId, err = createDummyAcl(dashboardPermission, search, dashboard.Id) - if !assert.Equal(t, nil, err) { - return + dashboard := createDummyDashboard(t, sqlStore, dashboardProps) + + var aclUserID int64 + if dashboardPermission != nil { + aclUserID = createDummyACL(t, sqlStore, dashboardPermission, search, dashboard.Id) + t.Logf("Created ACL with user ID %d\n", aclUserID) } - } - dashboards, err := getDashboards(sqlStore, search, aclUserId) - if !assert.Equal(t, nil, err) { - return - } + dashboards := getDashboards(t, sqlStore, search, aclUserID) - if shouldFind { - if assert.Equal(t, 1, len(dashboards), "Should return one dashboard") { - assert.Equal(t, dashboards[0].Id, dashboard.Id, "Should return created dashboard") + if shouldFind { + require.Len(t, dashboards, 1, "Should return one dashboard") + assert.Equal(t, dashboard.Id, dashboards[0].Id, "Should return created dashboard") + } else { + assert.Empty(t, dashboards, "Should not return any dashboard") } - } else { - assert.Equal(t, 0, len(dashboards), "Should node return any dashboard") - } + }) } -func createDummyUser() (*models.User, error) { +func createDummyUser(t *testing.T, sqlStore *SQLStore) *models.User { + t.Helper() + uid := strconv.Itoa(rand.Intn(9999999)) - createUserCmd := &models.CreateUserCommand{ + createUserCmd := models.CreateUserCommand{ Email: uid + "@example.com", Login: uid, Name: uid, @@ -207,33 +205,28 @@ func createDummyUser() (*models.User, error) { SkipOrgSetup: false, DefaultOrgRole: string(models.ROLE_VIEWER), } - err := CreateUser(context.Background(), createUserCmd) - if err != nil { - return nil, err - } + user, err := sqlStore.CreateUser(context.Background(), createUserCmd) + require.NoError(t, err) - return &createUserCmd.Result, nil + return user } -func createDummyTeam() (*models.Team, error) { - cmd := &models.CreateTeamCommand{ - // Does not matter in this tests actually - OrgId: 1, - Name: "test", - Email: "test@example.com", - } - err := CreateTeam(cmd) - if err != nil { - return nil, err - } +func createDummyTeam(t *testing.T, sqlStore *SQLStore) models.Team { + t.Helper() - return &cmd.Result, nil + team, err := sqlStore.CreateTeam("test", "test@example.com", 1) + require.NoError(t, err) + + return team } -func createDummyDashboard(dashboardProps DashboardProps) (*models.Dashboard, error) { - json, _ := simplejson.NewJson([]byte(`{"schemaVersion":17,"title":"gdev dashboards","uid":"","version":1}`)) +func createDummyDashboard(t *testing.T, sqlStore *SQLStore, dashboardProps DashboardProps) *models.Dashboard { + t.Helper() - saveDashboardCmd := &models.SaveDashboardCommand{ + json, err := simplejson.NewJson([]byte(`{"schemaVersion":17,"title":"gdev dashboards","uid":"","version":1}`)) + require.NoError(t, err) + + saveDashboardCmd := models.SaveDashboardCommand{ Dashboard: json, UserId: 0, Overwrite: false, @@ -250,53 +243,40 @@ func createDummyDashboard(dashboardProps DashboardProps) (*models.Dashboard, err saveDashboardCmd.OrgId = 1 } - err := SaveDashboard(saveDashboardCmd) - if err != nil { - return nil, err - } + dash, err := sqlStore.SaveDashboard(saveDashboardCmd) + require.NoError(t, err) - return saveDashboardCmd.Result, nil + t.Logf("Created dashboard with ID %d and org ID %d\n", dash.Id, dash.OrgId) + return dash } -func createDummyAcl(dashboardPermission *DashboardPermission, search Search, dashboardId int64) (int64, error) { +func createDummyACL(t *testing.T, sqlStore *SQLStore, dashboardPermission *DashboardPermission, search Search, dashboardID int64) int64 { + t.Helper() + acl := &models.DashboardAcl{ OrgID: 1, Created: time.Now(), Updated: time.Now(), Permission: dashboardPermission.Permission, - DashboardID: dashboardId, + DashboardID: dashboardID, } var user *models.User - var err error if dashboardPermission.User { - user, err = createDummyUser() - if err != nil { - return 0, err - } + t.Logf("Creating user") + user = createDummyUser(t, sqlStore) acl.UserID = user.Id } if dashboardPermission.Team { - team, err := createDummyTeam() - if err != nil { - return 0, err - } + t.Logf("Creating team") + team := createDummyTeam(t, sqlStore) if search.UserFromACL { - user, err = createDummyUser() - if err != nil { - return 0, err - } - addTeamMemberCmd := &models.AddTeamMemberCommand{ - UserId: user.Id, - OrgId: 1, - TeamId: team.Id, - } - err = AddTeamMember(addTeamMemberCmd) - if err != nil { - return 0, err - } + user = createDummyUser(t, sqlStore) + err := sqlStore.AddTeamMember(user.Id, 1, team.Id, false, 0) + require.NoError(t, err) + t.Logf("Created team member with ID %d", user.Id) } acl.TeamID = team.Id @@ -306,18 +286,17 @@ func createDummyAcl(dashboardPermission *DashboardPermission, search Search, das acl.Role = &dashboardPermission.Role } - updateAclCmd := &models.UpdateDashboardAclCommand{ - DashboardID: dashboardId, - Items: []*models.DashboardAcl{acl}, - } - err = UpdateDashboardAcl(updateAclCmd) + err := sqlStore.UpdateDashboardACL(dashboardID, []*models.DashboardAcl{acl}) + require.NoError(t, err) if user != nil { - return user.Id, err + return user.Id } - return 0, err + return 0 } -func getDashboards(sqlStore *SQLStore, search Search, aclUserId int64) ([]*dashboardResponse, error) { +func getDashboards(t *testing.T, sqlStore *SQLStore, search Search, aclUserID int64) []*dashboardResponse { + t.Helper() + builder := &SQLBuilder{} signedInUser := &models.SignedInUser{ UserId: 9999999999, @@ -335,12 +314,14 @@ func getDashboards(sqlStore *SQLStore, search Search, aclUserId int64) ([]*dashb signedInUser.OrgRole = models.ROLE_VIEWER } if search.UserFromACL { - signedInUser.UserId = aclUserId + signedInUser.UserId = aclUserID } var res []*dashboardResponse builder.Write("SELECT * FROM dashboard WHERE true") builder.WriteDashboardPermissionFilter(signedInUser, search.RequiredPermission) + t.Logf("Searching for dashboards, SQL: %q\n", builder.GetSQLString()) err := sqlStore.engine.SQL(builder.GetSQLString(), builder.params...).Find(&res) - return res, err + require.NoError(t, err) + return res } diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index c8ac9b2294d..ea8148c95f8 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -22,7 +22,6 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" "github.com/grafana/grafana/pkg/setting" - _ "github.com/grafana/grafana/pkg/tsdb/mssql" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/errutil" _ "github.com/lib/pq" diff --git a/pkg/services/sqlstore/stats_test.go b/pkg/services/sqlstore/stats_test.go index 3514d94db82..60f073223fc 100644 --- a/pkg/services/sqlstore/stats_test.go +++ b/pkg/services/sqlstore/stats_test.go @@ -13,8 +13,8 @@ import ( ) func TestStatsDataAccess(t *testing.T) { - InitTestDB(t) - populateDB(t) + sqlStore := InitTestDB(t) + populateDB(t, sqlStore) t.Run("Get system stats should not results in error", func(t *testing.T) { query := models.GetSystemStatsQuery{} @@ -57,18 +57,20 @@ func TestStatsDataAccess(t *testing.T) { }) } -func populateDB(t *testing.T) { +func populateDB(t *testing.T, sqlStore *SQLStore) { + t.Helper() + users := make([]models.User, 3) for i := range users { - cmd := &models.CreateUserCommand{ + cmd := models.CreateUserCommand{ Email: fmt.Sprintf("usertest%v@test.com", i), Name: fmt.Sprintf("user name %v", i), Login: fmt.Sprintf("user_test_%v_login", i), OrgName: fmt.Sprintf("Org #%v", i), } - err := CreateUser(context.Background(), cmd) + user, err := sqlStore.CreateUser(context.Background(), cmd) require.NoError(t, err) - users[i] = cmd.Result + users[i] = *user } // get 1st user's organisation diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index b04ec56cc3c..50f6ec4f119 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -2,6 +2,7 @@ package sqlstore import ( "bytes" + "context" "fmt" "strings" "time" @@ -11,14 +12,12 @@ import ( ) func init() { - bus.AddHandler("sql", CreateTeam) bus.AddHandler("sql", UpdateTeam) bus.AddHandler("sql", DeleteTeam) bus.AddHandler("sql", SearchTeams) bus.AddHandler("sql", GetTeamById) bus.AddHandler("sql", GetTeamsByUser) - bus.AddHandler("sql", AddTeamMember) bus.AddHandler("sql", UpdateTeamMember) bus.AddHandler("sql", RemoveTeamMember) bus.AddHandler("sql", GetTeamMembers) @@ -43,7 +42,7 @@ func getFilteredUsers(signedInUser *models.SignedInUser, hiddenUsers map[string] func getTeamMemberCount(filteredUsers []string) string { if len(filteredUsers) > 0 { - return `(SELECT COUNT(*) FROM team_member + return `(SELECT COUNT(*) FROM team_member INNER JOIN ` + dialect.Quote("user") + ` ON team_member.user_id = ` + dialect.Quote("user") + `.id WHERE team_member.team_id = team.id AND ` + dialect.Quote("user") + `.login NOT IN (?` + strings.Repeat(",?", len(filteredUsers)-1) + ")" + @@ -75,28 +74,25 @@ func getTeamSelectSQLBase(filteredUsers []string) string { ` FROM team as team ` } -func CreateTeam(cmd *models.CreateTeamCommand) error { - return inTransaction(func(sess *DBSession) error { - if isNameTaken, err := isTeamNameTaken(cmd.OrgId, cmd.Name, 0, sess); err != nil { +func (ss *SQLStore) CreateTeam(name, email string, orgID int64) (models.Team, error) { + team := models.Team{ + Name: name, + Email: email, + OrgId: orgID, + Created: time.Now(), + Updated: time.Now(), + } + err := ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error { + if isNameTaken, err := isTeamNameTaken(orgID, name, 0, sess); err != nil { return err } else if isNameTaken { return models.ErrTeamNameTaken } - team := models.Team{ - Name: cmd.Name, - Email: cmd.Email, - OrgId: cmd.OrgId, - Created: time.Now(), - Updated: time.Now(), - } - _, err := sess.Insert(&team) - - cmd.Result = team - return err }) + return team, err } func UpdateTeam(cmd *models.UpdateTeamCommand) error { @@ -152,8 +148,8 @@ func DeleteTeam(cmd *models.DeleteTeamCommand) error { }) } -func teamExists(orgId int64, teamId int64, sess *DBSession) (bool, error) { - if res, err := sess.Query("SELECT 1 from team WHERE org_id=? and id=?", orgId, teamId); err != nil { +func teamExists(orgID int64, teamID int64, sess *DBSession) (bool, error) { + if res, err := sess.Query("SELECT 1 from team WHERE org_id=? and id=?", orgID, teamID); err != nil { return false, err } else if len(res) != 1 { return false, models.ErrTeamNotFound @@ -165,7 +161,6 @@ func teamExists(orgId int64, teamId int64, sess *DBSession) (bool, error) { func isTeamNameTaken(orgId int64, name string, existingId int64, sess *DBSession) (bool, error) { var team models.Team exists, err := sess.Where("org_id=? and name=?", orgId, name).Get(&team) - if err != nil { return false, nil } @@ -283,26 +278,27 @@ func GetTeamsByUser(query *models.GetTeamsByUserQuery) error { } // AddTeamMember adds a user to a team -func AddTeamMember(cmd *models.AddTeamMemberCommand) error { - return inTransaction(func(sess *DBSession) error { - if res, err := sess.Query("SELECT 1 from team_member WHERE org_id=? and team_id=? and user_id=?", cmd.OrgId, cmd.TeamId, cmd.UserId); err != nil { +func (ss *SQLStore) AddTeamMember(userID, orgID, teamID int64, isExternal bool, permission models.PermissionType) error { + return ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error { + if res, err := sess.Query("SELECT 1 from team_member WHERE org_id=? and team_id=? and user_id=?", + orgID, teamID, userID); err != nil { return err } else if len(res) == 1 { return models.ErrTeamMemberAlreadyAdded } - if _, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil { + if _, err := teamExists(orgID, teamID, sess); err != nil { return err } entity := models.TeamMember{ - OrgId: cmd.OrgId, - TeamId: cmd.TeamId, - UserId: cmd.UserId, - External: cmd.External, + OrgId: orgID, + TeamId: teamID, + UserId: userID, + External: isExternal, Created: time.Now(), Updated: time.Now(), - Permission: cmd.Permission, + Permission: permission, } _, err := sess.Insert(&entity) diff --git a/pkg/services/sqlstore/team_test.go b/pkg/services/sqlstore/team_test.go index e56985ee7ba..80af6cdba0e 100644 --- a/pkg/services/sqlstore/team_test.go +++ b/pkg/services/sqlstore/team_test.go @@ -14,32 +14,29 @@ import ( func TestTeamCommandsAndQueries(t *testing.T) { Convey("Testing Team commands & queries", t, func() { - InitTestDB(t) + sqlStore := InitTestDB(t) Convey("Given saved users and two teams", func() { var userIds []int64 for i := 0; i < 5; i++ { - userCmd := &models.CreateUserCommand{ + userCmd := models.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), Name: fmt.Sprint("user", i), Login: fmt.Sprint("loginuser", i), } - err := CreateUser(context.Background(), userCmd) + user, err := sqlStore.CreateUser(context.Background(), userCmd) So(err, ShouldBeNil) - userIds = append(userIds, userCmd.Result.Id) + userIds = append(userIds, user.Id) } - var testOrgId int64 = 1 - group1 := models.CreateTeamCommand{OrgId: testOrgId, Name: "group1 name", Email: "test1@test.com"} - group2 := models.CreateTeamCommand{OrgId: testOrgId, Name: "group2 name", Email: "test2@test.com"} - - err := CreateTeam(&group1) + const testOrgID int64 = 1 + team1, err := sqlStore.CreateTeam("group1 name", "test1@test.com", testOrgID) So(err, ShouldBeNil) - err = CreateTeam(&group2) + team2, err := sqlStore.CreateTeam("group2 name", "test2@test.com", testOrgID) So(err, ShouldBeNil) Convey("Should be able to create teams and add users", func() { - query := &models.SearchTeamsQuery{OrgId: testOrgId, Name: "group1 name", Page: 1, Limit: 10} + query := &models.SearchTeamsQuery{OrgId: testOrgID, Name: "group1 name", Page: 1, Limit: 10} err = SearchTeams(query) So(err, ShouldBeNil) So(query.Page, ShouldEqual, 1) @@ -47,33 +44,33 @@ func TestTeamCommandsAndQueries(t *testing.T) { team1 := query.Result.Teams[0] So(team1.Name, ShouldEqual, "group1 name") So(team1.Email, ShouldEqual, "test1@test.com") - So(team1.OrgId, ShouldEqual, testOrgId) + So(team1.OrgId, ShouldEqual, testOrgID) So(team1.MemberCount, ShouldEqual, 0) - err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team1.Id, UserId: userIds[0]}) + err = sqlStore.AddTeamMember(userIds[0], testOrgID, team1.Id, false, 0) So(err, ShouldBeNil) - err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team1.Id, UserId: userIds[1], External: true}) + err = sqlStore.AddTeamMember(userIds[1], testOrgID, team1.Id, true, 0) So(err, ShouldBeNil) - q1 := &models.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team1.Id} + q1 := &models.GetTeamMembersQuery{OrgId: testOrgID, TeamId: team1.Id} err = GetTeamMembers(q1) So(err, ShouldBeNil) So(q1.Result, ShouldHaveLength, 2) So(q1.Result[0].TeamId, ShouldEqual, team1.Id) So(q1.Result[0].Login, ShouldEqual, "loginuser0") - So(q1.Result[0].OrgId, ShouldEqual, testOrgId) + So(q1.Result[0].OrgId, ShouldEqual, testOrgID) So(q1.Result[1].TeamId, ShouldEqual, team1.Id) So(q1.Result[1].Login, ShouldEqual, "loginuser1") - So(q1.Result[1].OrgId, ShouldEqual, testOrgId) + So(q1.Result[1].OrgId, ShouldEqual, testOrgID) So(q1.Result[1].External, ShouldEqual, true) - q2 := &models.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team1.Id, External: true} + q2 := &models.GetTeamMembersQuery{OrgId: testOrgID, TeamId: team1.Id, External: true} err = GetTeamMembers(q2) So(err, ShouldBeNil) So(q2.Result, ShouldHaveLength, 1) So(q2.Result[0].TeamId, ShouldEqual, team1.Id) So(q2.Result[0].Login, ShouldEqual, "loginuser1") - So(q2.Result[0].OrgId, ShouldEqual, testOrgId) + So(q2.Result[0].OrgId, ShouldEqual, testOrgID) So(q2.Result[0].External, ShouldEqual, true) err = SearchTeams(query) @@ -81,13 +78,13 @@ func TestTeamCommandsAndQueries(t *testing.T) { team1 = query.Result.Teams[0] So(team1.MemberCount, ShouldEqual, 2) - getTeamQuery := &models.GetTeamByIdQuery{OrgId: testOrgId, Id: team1.Id} + getTeamQuery := &models.GetTeamByIdQuery{OrgId: testOrgID, Id: team1.Id} err = GetTeamById(getTeamQuery) So(err, ShouldBeNil) team1 = getTeamQuery.Result So(team1.Name, ShouldEqual, "group1 name") So(team1.Email, ShouldEqual, "test1@test.com") - So(team1.OrgId, ShouldEqual, testOrgId) + So(team1.OrgId, ShouldEqual, testOrgID) So(team1.MemberCount, ShouldEqual, 2) }) @@ -96,49 +93,48 @@ func TestTeamCommandsAndQueries(t *testing.T) { err := SetAuthInfo(&models.SetAuthInfoCommand{UserId: userId, AuthModule: "oauth_github", AuthId: "1234567"}) So(err, ShouldBeNil) - teamQuery := &models.SearchTeamsQuery{OrgId: testOrgId, Name: "group1 name", Page: 1, Limit: 10} + teamQuery := &models.SearchTeamsQuery{OrgId: testOrgID, Name: "group1 name", Page: 1, Limit: 10} err = SearchTeams(teamQuery) So(err, ShouldBeNil) So(teamQuery.Page, ShouldEqual, 1) team1 := teamQuery.Result.Teams[0] - err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team1.Id, UserId: userId, External: true}) + err = sqlStore.AddTeamMember(userId, testOrgID, team1.Id, true, 0) So(err, ShouldBeNil) - memberQuery := &models.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team1.Id, External: true} + memberQuery := &models.GetTeamMembersQuery{OrgId: testOrgID, TeamId: team1.Id, External: true} err = GetTeamMembers(memberQuery) So(err, ShouldBeNil) So(memberQuery.Result, ShouldHaveLength, 1) So(memberQuery.Result[0].TeamId, ShouldEqual, team1.Id) So(memberQuery.Result[0].Login, ShouldEqual, "loginuser1") - So(memberQuery.Result[0].OrgId, ShouldEqual, testOrgId) + So(memberQuery.Result[0].OrgId, ShouldEqual, testOrgID) So(memberQuery.Result[0].External, ShouldEqual, true) So(memberQuery.Result[0].AuthModule, ShouldEqual, "oauth_github") }) Convey("Should be able to update users in a team", func() { userId := userIds[0] - team := group1.Result - addMemberCmd := models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team.Id, UserId: userId} - err = AddTeamMember(&addMemberCmd) + team := team1 + err = sqlStore.AddTeamMember(userId, testOrgID, team.Id, false, 0) So(err, ShouldBeNil) - qBeforeUpdate := &models.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team.Id} + qBeforeUpdate := &models.GetTeamMembersQuery{OrgId: testOrgID, TeamId: team.Id} err = GetTeamMembers(qBeforeUpdate) So(err, ShouldBeNil) So(qBeforeUpdate.Result[0].Permission, ShouldEqual, 0) err = UpdateTeamMember(&models.UpdateTeamMemberCommand{ UserId: userId, - OrgId: testOrgId, + OrgId: testOrgID, TeamId: team.Id, Permission: models.PERMISSION_ADMIN, }) So(err, ShouldBeNil) - qAfterUpdate := &models.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team.Id} + qAfterUpdate := &models.GetTeamMembersQuery{OrgId: testOrgID, TeamId: team.Id} err = GetTeamMembers(qAfterUpdate) So(err, ShouldBeNil) So(qAfterUpdate.Result[0].Permission, ShouldEqual, models.PERMISSION_ADMIN) @@ -146,12 +142,11 @@ func TestTeamCommandsAndQueries(t *testing.T) { Convey("Should default to member permission level when updating a user with invalid permission level", func() { userID := userIds[0] - team := group1.Result - addMemberCmd := models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team.Id, UserId: userID} - err = AddTeamMember(&addMemberCmd) + team := team1 + err = sqlStore.AddTeamMember(userID, testOrgID, team.Id, false, 0) So(err, ShouldBeNil) - qBeforeUpdate := &models.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team.Id} + qBeforeUpdate := &models.GetTeamMembersQuery{OrgId: testOrgID, TeamId: team.Id} err = GetTeamMembers(qBeforeUpdate) So(err, ShouldBeNil) So(qBeforeUpdate.Result[0].Permission, ShouldEqual, 0) @@ -159,14 +154,14 @@ func TestTeamCommandsAndQueries(t *testing.T) { invalidPermissionLevel := models.PERMISSION_EDIT err = UpdateTeamMember(&models.UpdateTeamMemberCommand{ UserId: userID, - OrgId: testOrgId, + OrgId: testOrgID, TeamId: team.Id, Permission: invalidPermissionLevel, }) So(err, ShouldBeNil) - qAfterUpdate := &models.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team.Id} + qAfterUpdate := &models.GetTeamMembersQuery{OrgId: testOrgID, TeamId: team.Id} err = GetTeamMembers(qAfterUpdate) So(err, ShouldBeNil) So(qAfterUpdate.Result[0].Permission, ShouldEqual, 0) @@ -175,8 +170,8 @@ func TestTeamCommandsAndQueries(t *testing.T) { Convey("Shouldn't be able to update a user not in the team.", func() { err = UpdateTeamMember(&models.UpdateTeamMemberCommand{ UserId: 1, - OrgId: testOrgId, - TeamId: group1.Result.Id, + OrgId: testOrgID, + TeamId: team1.Id, Permission: models.PERMISSION_ADMIN, }) @@ -184,24 +179,24 @@ func TestTeamCommandsAndQueries(t *testing.T) { }) Convey("Should be able to search for teams", func() { - query := &models.SearchTeamsQuery{OrgId: testOrgId, Query: "group", Page: 1} + query := &models.SearchTeamsQuery{OrgId: testOrgID, Query: "group", Page: 1} err = SearchTeams(query) So(err, ShouldBeNil) So(len(query.Result.Teams), ShouldEqual, 2) So(query.Result.TotalCount, ShouldEqual, 2) - query2 := &models.SearchTeamsQuery{OrgId: testOrgId, Query: ""} + query2 := &models.SearchTeamsQuery{OrgId: testOrgID, Query: ""} err = SearchTeams(query2) So(err, ShouldBeNil) So(len(query2.Result.Teams), ShouldEqual, 2) }) Convey("Should be able to return all teams a user is member of", func() { - groupId := group2.Result.Id - err := AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[0]}) + groupId := team2.Id + err := sqlStore.AddTeamMember(userIds[0], testOrgID, groupId, false, 0) So(err, ShouldBeNil) - query := &models.GetTeamsByUserQuery{OrgId: testOrgId, UserId: userIds[0]} + query := &models.GetTeamsByUserQuery{OrgId: testOrgID, UserId: userIds[0]} err = GetTeamsByUser(query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 1) @@ -210,63 +205,65 @@ func TestTeamCommandsAndQueries(t *testing.T) { }) Convey("Should be able to remove users from a group", func() { - err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0]}) + err = sqlStore.AddTeamMember(userIds[0], testOrgID, team1.Id, false, 0) So(err, ShouldBeNil) - err = RemoveTeamMember(&models.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0]}) + err = RemoveTeamMember(&models.RemoveTeamMemberCommand{OrgId: testOrgID, TeamId: team1.Id, UserId: userIds[0]}) So(err, ShouldBeNil) - q2 := &models.GetTeamMembersQuery{OrgId: testOrgId, TeamId: group1.Result.Id} + q2 := &models.GetTeamMembersQuery{OrgId: testOrgID, TeamId: team1.Id} err = GetTeamMembers(q2) So(err, ShouldBeNil) So(len(q2.Result), ShouldEqual, 0) }) Convey("When ProtectLastAdmin is set to true", func() { - err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], Permission: models.PERMISSION_ADMIN}) + err = sqlStore.AddTeamMember(userIds[0], testOrgID, team1.Id, false, models.PERMISSION_ADMIN) So(err, ShouldBeNil) Convey("A user should not be able to remove the last admin", func() { - err = RemoveTeamMember(&models.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], ProtectLastAdmin: true}) + err = RemoveTeamMember(&models.RemoveTeamMemberCommand{OrgId: testOrgID, TeamId: team1.Id, UserId: userIds[0], ProtectLastAdmin: true}) So(err, ShouldEqual, models.ErrLastTeamAdmin) }) Convey("A user should be able to remove an admin if there are other admins", func() { - err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[1], Permission: models.PERMISSION_ADMIN}) + err = sqlStore.AddTeamMember(userIds[1], testOrgID, team1.Id, false, models.PERMISSION_ADMIN) So(err, ShouldBeNil) - err = RemoveTeamMember(&models.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], ProtectLastAdmin: true}) + err = RemoveTeamMember(&models.RemoveTeamMemberCommand{OrgId: testOrgID, TeamId: team1.Id, UserId: userIds[0], ProtectLastAdmin: true}) So(err, ShouldBeNil) }) Convey("A user should not be able to remove the admin permission for the last admin", func() { - err = UpdateTeamMember(&models.UpdateTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], Permission: 0, ProtectLastAdmin: true}) + err = UpdateTeamMember(&models.UpdateTeamMemberCommand{OrgId: testOrgID, TeamId: team1.Id, UserId: userIds[0], Permission: 0, ProtectLastAdmin: true}) So(err, ShouldBeError, models.ErrLastTeamAdmin) }) Convey("A user should be able to remove the admin permission if there are other admins", func() { - err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[1], Permission: models.PERMISSION_ADMIN}) + err = sqlStore.AddTeamMember(userIds[1], testOrgID, team1.Id, false, models.PERMISSION_ADMIN) So(err, ShouldBeNil) - err = UpdateTeamMember(&models.UpdateTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0], Permission: 0, ProtectLastAdmin: true}) + err = UpdateTeamMember(&models.UpdateTeamMemberCommand{OrgId: testOrgID, TeamId: team1.Id, UserId: userIds[0], Permission: 0, ProtectLastAdmin: true}) So(err, ShouldBeNil) }) }) Convey("Should be able to remove a group with users and permissions", func() { - groupId := group2.Result.Id - err := AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[1]}) + groupId := team2.Id + err := sqlStore.AddTeamMember(userIds[1], testOrgID, groupId, false, 0) So(err, ShouldBeNil) - err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[2]}) + err = sqlStore.AddTeamMember(userIds[2], testOrgID, groupId, false, 0) So(err, ShouldBeNil) - err = testHelperUpdateDashboardAcl(1, models.DashboardAcl{DashboardID: 1, OrgID: testOrgId, Permission: models.PERMISSION_EDIT, TeamID: groupId}) + err = testHelperUpdateDashboardAcl(t, sqlStore, 1, models.DashboardAcl{ + DashboardID: 1, OrgID: testOrgID, Permission: models.PERMISSION_EDIT, TeamID: groupId, + }) So(err, ShouldBeNil) - err = DeleteTeam(&models.DeleteTeamCommand{OrgId: testOrgId, Id: groupId}) + err = DeleteTeam(&models.DeleteTeamCommand{OrgId: testOrgID, Id: groupId}) So(err, ShouldBeNil) - query := &models.GetTeamByIdQuery{OrgId: testOrgId, Id: groupId} + query := &models.GetTeamByIdQuery{OrgId: testOrgID, Id: groupId} err = GetTeamById(query) So(err, ShouldEqual, models.ErrTeamNotFound) - permQuery := &models.GetDashboardAclInfoListQuery{DashboardID: 1, OrgID: testOrgId} + permQuery := &models.GetDashboardAclInfoListQuery{DashboardID: 1, OrgID: testOrgID} err = GetDashboardAclInfoList(permQuery) So(err, ShouldBeNil) @@ -274,18 +271,18 @@ func TestTeamCommandsAndQueries(t *testing.T) { }) Convey("Should be able to return if user is admin of teams or not", func() { - groupId := group2.Result.Id - err := AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[0]}) + groupId := team2.Id + err := sqlStore.AddTeamMember(userIds[0], testOrgID, groupId, false, 0) So(err, ShouldBeNil) - err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[1], Permission: models.PERMISSION_ADMIN}) + err = sqlStore.AddTeamMember(userIds[1], testOrgID, groupId, false, models.PERMISSION_ADMIN) So(err, ShouldBeNil) - query := &models.IsAdminOfTeamsQuery{SignedInUser: &models.SignedInUser{OrgId: testOrgId, UserId: userIds[0]}} + query := &models.IsAdminOfTeamsQuery{SignedInUser: &models.SignedInUser{OrgId: testOrgID, UserId: userIds[0]}} err = IsAdminOfTeams(query) So(err, ShouldBeNil) So(query.Result, ShouldBeFalse) - query = &models.IsAdminOfTeamsQuery{SignedInUser: &models.SignedInUser{OrgId: testOrgId, UserId: userIds[1]}} + query = &models.IsAdminOfTeamsQuery{SignedInUser: &models.SignedInUser{OrgId: testOrgID, UserId: userIds[1]}} err = IsAdminOfTeams(query) So(err, ShouldBeNil) So(query.Result, ShouldBeTrue) @@ -295,29 +292,29 @@ func TestTeamCommandsAndQueries(t *testing.T) { signedInUser := &models.SignedInUser{Login: "loginuser0"} hiddenUsers := map[string]struct{}{"loginuser0": {}, "loginuser1": {}} - teamId := group1.Result.Id - err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: teamId, UserId: userIds[0]}) + teamId := team1.Id + err = sqlStore.AddTeamMember(userIds[0], testOrgID, teamId, false, 0) So(err, ShouldBeNil) - err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: teamId, UserId: userIds[1]}) + err = sqlStore.AddTeamMember(userIds[1], testOrgID, teamId, false, 0) So(err, ShouldBeNil) - err = AddTeamMember(&models.AddTeamMemberCommand{OrgId: testOrgId, TeamId: teamId, UserId: userIds[2]}) + err = sqlStore.AddTeamMember(userIds[2], testOrgID, teamId, false, 0) So(err, ShouldBeNil) - searchQuery := &models.SearchTeamsQuery{OrgId: testOrgId, Page: 1, Limit: 10, SignedInUser: signedInUser, HiddenUsers: hiddenUsers} + searchQuery := &models.SearchTeamsQuery{OrgId: testOrgID, Page: 1, Limit: 10, SignedInUser: signedInUser, HiddenUsers: hiddenUsers} err = SearchTeams(searchQuery) So(err, ShouldBeNil) So(searchQuery.Result.Teams, ShouldHaveLength, 2) team1 := searchQuery.Result.Teams[0] So(team1.MemberCount, ShouldEqual, 2) - searchQueryFilteredByUser := &models.SearchTeamsQuery{OrgId: testOrgId, Page: 1, Limit: 10, UserIdFilter: userIds[0], SignedInUser: signedInUser, HiddenUsers: hiddenUsers} + searchQueryFilteredByUser := &models.SearchTeamsQuery{OrgId: testOrgID, Page: 1, Limit: 10, UserIdFilter: userIds[0], SignedInUser: signedInUser, HiddenUsers: hiddenUsers} err = SearchTeams(searchQueryFilteredByUser) So(err, ShouldBeNil) So(searchQueryFilteredByUser.Result.Teams, ShouldHaveLength, 1) team1 = searchQuery.Result.Teams[0] So(team1.MemberCount, ShouldEqual, 2) - getTeamQuery := &models.GetTeamByIdQuery{OrgId: testOrgId, Id: teamId, SignedInUser: signedInUser, HiddenUsers: hiddenUsers} + getTeamQuery := &models.GetTeamByIdQuery{OrgId: testOrgID, Id: teamId, SignedInUser: signedInUser, HiddenUsers: hiddenUsers} err = GetTeamById(getTeamQuery) So(err, ShouldBeNil) So(getTeamQuery.Result.MemberCount, ShouldEqual, 2) diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 22bf350ef52..a4c936dab5f 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -31,12 +31,10 @@ func (ss *SQLStore) addUserQueryAndCommandHandlers() { bus.AddHandler("sql", DisableUser) bus.AddHandler("sql", BatchDisableUsers) bus.AddHandler("sql", DeleteUser) - bus.AddHandler("sql", UpdateUserPermissions) bus.AddHandler("sql", SetUserHelpFlag) - bus.AddHandlerCtx("sql", CreateUser) } -func getOrgIdForNewUser(sess *DBSession, cmd *models.CreateUserCommand) (int64, error) { +func getOrgIdForNewUser(sess *DBSession, cmd models.CreateUserCommand) (int64, error) { if cmd.SkipOrgSetup { return -1, nil } @@ -191,8 +189,9 @@ func (ss *SQLStore) createUser(ctx context.Context, args userCreationArgs, skipO return user, nil } -func CreateUser(ctx context.Context, cmd *models.CreateUserCommand) error { - return inTransactionCtx(ctx, func(sess *DBSession) error { +func (ss *SQLStore) CreateUser(ctx context.Context, cmd models.CreateUserCommand) (*models.User, error) { + var user *models.User + err := ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error { orgId, err := getOrgIdForNewUser(sess, cmd) if err != nil { return err @@ -202,13 +201,16 @@ func CreateUser(ctx context.Context, cmd *models.CreateUserCommand) error { cmd.Email = cmd.Login } - exists, _ := sess.Where("email=? OR login=?", cmd.Email, cmd.Login).Get(&models.User{}) + exists, err := sess.Where("email=? OR login=?", cmd.Email, cmd.Login).Get(&models.User{}) + if err != nil { + return err + } if exists { return models.ErrUserAlreadyExists } // create user - user := models.User{ + user = &models.User{ Email: cmd.Email, Name: cmd.Name, Login: cmd.Login, @@ -243,7 +245,7 @@ func CreateUser(ctx context.Context, cmd *models.CreateUserCommand) error { sess.UseBool("is_admin") - if _, err := sess.Insert(&user); err != nil { + if _, err := sess.Insert(user); err != nil { return err } @@ -255,8 +257,6 @@ func CreateUser(ctx context.Context, cmd *models.CreateUserCommand) error { Email: user.Email, }) - cmd.Result = user - // create org user link if !cmd.SkipOrgSetup { orgUser := models.OrgUser{ @@ -282,6 +282,8 @@ func CreateUser(ctx context.Context, cmd *models.CreateUserCommand) error { return nil }) + + return user, err } func GetUserById(query *models.GetUserByIdQuery) error { @@ -729,14 +731,14 @@ func deleteUserInTransaction(sess *DBSession, cmd *models.DeleteUserCommand) err return nil } -func UpdateUserPermissions(cmd *models.UpdateUserPermissionsCommand) error { - return inTransaction(func(sess *DBSession) error { - user := models.User{} - if _, err := sess.ID(cmd.UserId).Get(&user); err != nil { +func (ss *SQLStore) UpdateUserPermissions(userID int64, isAdmin bool) error { + return ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error { + var user models.User + if _, err := sess.ID(userID).Get(&user); err != nil { return err } - user.IsAdmin = cmd.IsGrafanaAdmin + user.IsAdmin = isAdmin sess.UseBool("is_admin") _, err := sess.ID(user.Id).Update(&user) diff --git a/pkg/services/sqlstore/user_auth_test.go b/pkg/services/sqlstore/user_auth_test.go index e5bb2379e5c..15f64decec3 100644 --- a/pkg/services/sqlstore/user_auth_test.go +++ b/pkg/services/sqlstore/user_auth_test.go @@ -15,18 +15,16 @@ import ( //nolint:goconst func TestUserAuth(t *testing.T) { - InitTestDB(t) + sqlStore := InitTestDB(t) Convey("Given 5 users", t, func() { - var err error - var cmd *models.CreateUserCommand for i := 0; i < 5; i++ { - cmd = &models.CreateUserCommand{ + cmd := models.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), Name: fmt.Sprint("user", i), Login: fmt.Sprint("loginuser", i), } - err = CreateUser(context.Background(), cmd) + _, err := sqlStore.CreateUser(context.Background(), cmd) So(err, ShouldBeNil) } @@ -46,7 +44,7 @@ func TestUserAuth(t *testing.T) { login := "loginuser0" query := &models.GetUserByAuthInfoQuery{Login: login} - err = GetUserByAuthInfo(query) + err := GetUserByAuthInfo(query) So(err, ShouldBeNil) So(query.Result.Login, ShouldEqual, login) @@ -82,7 +80,7 @@ func TestUserAuth(t *testing.T) { Convey("Can set & locate by AuthModule and AuthId", func() { // get nonexistent user_auth entry query := &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} - err = GetUserByAuthInfo(query) + err := GetUserByAuthInfo(query) So(err, ShouldEqual, models.ErrUserNotFound) So(query.Result, ShouldBeNil) @@ -144,7 +142,7 @@ func TestUserAuth(t *testing.T) { // Calling GetUserByAuthInfoQuery on an existing user will populate an entry in the user_auth table query := &models.GetUserByAuthInfoQuery{Login: login, AuthModule: "test", AuthId: "test"} - err = GetUserByAuthInfo(query) + err := GetUserByAuthInfo(query) So(err, ShouldBeNil) So(query.Result.Login, ShouldEqual, login) @@ -179,7 +177,7 @@ func TestUserAuth(t *testing.T) { // Make the first log-in during the past getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) } query := &models.GetUserByAuthInfoQuery{Login: login, AuthModule: "test1", AuthId: "test1"} - err = GetUserByAuthInfo(query) + err := GetUserByAuthInfo(query) getTime = time.Now So(err, ShouldBeNil) diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go index 7da19f0ef4c..ae93403bbb1 100644 --- a/pkg/services/sqlstore/user_test.go +++ b/pkg/services/sqlstore/user_test.go @@ -20,17 +20,17 @@ func TestUserDataAccess(t *testing.T) { ss := InitTestDB(t) Convey("Creates a user", func() { - cmd := &models.CreateUserCommand{ + cmd := models.CreateUserCommand{ Email: "usertest@test.com", Name: "user name", Login: "user_test_login", } - err := CreateUser(context.Background(), cmd) + user, err := ss.CreateUser(context.Background(), cmd) So(err, ShouldBeNil) Convey("Loading a user", func() { - query := models.GetUserByIdQuery{Id: cmd.Result.Id} + query := models.GetUserByIdQuery{Id: user.Id} err := GetUserById(&query) So(err, ShouldBeNil) @@ -43,18 +43,18 @@ func TestUserDataAccess(t *testing.T) { }) Convey("Creates disabled user", func() { - cmd := &models.CreateUserCommand{ + cmd := models.CreateUserCommand{ Email: "usertest@test.com", Name: "user name", Login: "user_test_login", IsDisabled: true, } - err := CreateUser(context.Background(), cmd) + user, err := ss.CreateUser(context.Background(), cmd) So(err, ShouldBeNil) Convey("Loading a user", func() { - query := models.GetUserByIdQuery{Id: cmd.Result.Id} + query := models.GetUserByIdQuery{Id: user.Id} err := GetUserById(&query) So(err, ShouldBeNil) @@ -78,18 +78,18 @@ func TestUserDataAccess(t *testing.T) { So(err, ShouldBeNil) Convey("Creates user assigned to other organization", func() { - cmd := &models.CreateUserCommand{ + cmd := models.CreateUserCommand{ Email: "usertest@test.com", Name: "user name", Login: "user_test_login", OrgId: orgCmd.Result.Id, } - err := CreateUser(context.Background(), cmd) + user, err := ss.CreateUser(context.Background(), cmd) So(err, ShouldBeNil) Convey("Loading a user", func() { - query := models.GetUserByIdQuery{Id: cmd.Result.Id} + query := models.GetUserByIdQuery{Id: user.Id} err := GetUserById(&query) So(err, ShouldBeNil) @@ -104,20 +104,20 @@ func TestUserDataAccess(t *testing.T) { Convey("Don't create user assigned to unknown organization", func() { const nonExistingOrgID = 10000 - cmd := &models.CreateUserCommand{ + cmd := models.CreateUserCommand{ Email: "usertest@test.com", Name: "user name", Login: "user_test_login", OrgId: nonExistingOrgID, } - err := CreateUser(context.Background(), cmd) + _, err := ss.CreateUser(context.Background(), cmd) So(err, ShouldEqual, models.ErrOrgNotFound) }) }) Convey("Given 5 users", func() { - users := createFiveTestUsers(func(i int) *models.CreateUserCommand { + users := createFiveTestUsers(t, ss, func(i int) *models.CreateUserCommand { return &models.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), Name: fmt.Sprint("user", i), @@ -238,7 +238,7 @@ func TestUserDataAccess(t *testing.T) { Convey("Can return list users based on their is_disabled flag", func() { ss = InitTestDB(t) - createFiveTestUsers(func(i int) *models.CreateUserCommand { + createFiveTestUsers(t, ss, func(i int) *models.CreateUserCommand { return &models.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), Name: fmt.Sprint("user", i), @@ -269,7 +269,7 @@ func TestUserDataAccess(t *testing.T) { So(third, ShouldBeTrue) ss = InitTestDB(t) - users = createFiveTestUsers(func(i int) *models.CreateUserCommand { + users = createFiveTestUsers(t, ss, func(i int) *models.CreateUserCommand { return &models.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), Name: fmt.Sprint("user", i), @@ -286,7 +286,7 @@ func TestUserDataAccess(t *testing.T) { }) So(err, ShouldBeNil) - err = testHelperUpdateDashboardAcl(1, models.DashboardAcl{ + err = testHelperUpdateDashboardAcl(t, ss, 1, models.DashboardAcl{ DashboardID: 1, OrgID: users[0].OrgId, UserID: users[1].Id, Permission: models.PERMISSION_EDIT, }) @@ -365,7 +365,7 @@ func TestUserDataAccess(t *testing.T) { Convey("Should enable all users", func() { ss = InitTestDB(t) - createFiveTestUsers(func(i int) *models.CreateUserCommand { + createFiveTestUsers(t, ss, func(i int) *models.CreateUserCommand { return &models.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), Name: fmt.Sprint("user", i), @@ -392,7 +392,7 @@ func TestUserDataAccess(t *testing.T) { Convey("Should disable only specific users", func() { ss = InitTestDB(t) - users = createFiveTestUsers(func(i int) *models.CreateUserCommand { + users = createFiveTestUsers(t, ss, func(i int) *models.CreateUserCommand { return &models.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), Name: fmt.Sprint("user", i), @@ -438,7 +438,7 @@ func TestUserDataAccess(t *testing.T) { // Since previous tests were destructive ss = InitTestDB(t) - users = createFiveTestUsers(func(i int) *models.CreateUserCommand { + users = createFiveTestUsers(t, ss, func(i int) *models.CreateUserCommand { return &models.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), Name: fmt.Sprint("user", i), @@ -546,25 +546,22 @@ func TestUserDataAccess(t *testing.T) { }) Convey("Given one grafana admin user", func() { - var err error - createUserCmd := &models.CreateUserCommand{ + createUserCmd := models.CreateUserCommand{ Email: fmt.Sprint("admin", "@test.com"), Name: "admin", Login: "admin", IsAdmin: true, } - err = CreateUser(context.Background(), createUserCmd) + user, err := ss.CreateUser(context.Background(), createUserCmd) So(err, ShouldBeNil) Convey("Cannot make themselves a non-admin", func() { - updateUserPermsCmd := models.UpdateUserPermissionsCommand{IsGrafanaAdmin: false, UserId: 1} - updatePermsError := UpdateUserPermissions(&updateUserPermsCmd) + updatePermsError := ss.UpdateUserPermissions(1, false) So(updatePermsError, ShouldEqual, models.ErrLastGrafanaAdmin) - query := models.GetUserByIdQuery{Id: createUserCmd.Result.Id} + query := models.GetUserByIdQuery{Id: user.Id} getUserError := GetUserById(&query) - So(getUserError, ShouldBeNil) So(query.Result.IsAdmin, ShouldEqual, true) @@ -574,33 +571,33 @@ func TestUserDataAccess(t *testing.T) { Convey("Given one user", func() { const email = "user@test.com" const username = "user" - createUserCmd := &models.CreateUserCommand{ + createUserCmd := models.CreateUserCommand{ Email: email, Name: "user", Login: username, } - err := CreateUser(context.Background(), createUserCmd) + _, err := ss.CreateUser(context.Background(), createUserCmd) So(err, ShouldBeNil) Convey("When trying to create a new user with the same email, an error is returned", func() { - createUserCmd := &models.CreateUserCommand{ + createUserCmd := models.CreateUserCommand{ Email: email, Name: "user2", Login: "user2", SkipOrgSetup: true, } - err := CreateUser(context.Background(), createUserCmd) + _, err := ss.CreateUser(context.Background(), createUserCmd) So(err, ShouldEqual, models.ErrUserAlreadyExists) }) Convey("When trying to create a new user with the same login, an error is returned", func() { - createUserCmd := &models.CreateUserCommand{ + createUserCmd := models.CreateUserCommand{ Email: "user2@test.com", Name: "user2", Login: username, SkipOrgSetup: true, } - err := CreateUser(context.Background(), createUserCmd) + _, err := ss.CreateUser(context.Background(), createUserCmd) So(err, ShouldEqual, models.ErrUserAlreadyExists) }) }) @@ -618,15 +615,15 @@ func GetOrgUsersForTest(query *models.GetOrgUsersQuery) error { return err } -func createFiveTestUsers(fn func(i int) *models.CreateUserCommand) []models.User { - var err error - var cmd *models.CreateUserCommand +func createFiveTestUsers(t *testing.T, sqlStore *SQLStore, fn func(i int) *models.CreateUserCommand) []models.User { + t.Helper() + users := []models.User{} for i := 0; i < 5; i++ { - cmd = fn(i) + cmd := fn(i) - err = CreateUser(context.Background(), cmd) - users = append(users, cmd.Result) + user, err := sqlStore.CreateUser(context.Background(), *cmd) + users = append(users, *user) So(err, ShouldBeNil) } diff --git a/pkg/tsdb/azuremonitor/applicationinsights-datasource.go b/pkg/tsdb/azuremonitor/applicationinsights-datasource.go index a369ca0786e..3ee7ff9912b 100644 --- a/pkg/tsdb/azuremonitor/applicationinsights-datasource.go +++ b/pkg/tsdb/azuremonitor/applicationinsights-datasource.go @@ -18,7 +18,6 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util/errutil" "github.com/opentracing/opentracing-go" @@ -27,8 +26,9 @@ import ( // ApplicationInsightsDatasource calls the application insights query API. type ApplicationInsightsDatasource struct { - httpClient *http.Client - dsInfo *models.DataSource + httpClient *http.Client + dsInfo *models.DataSource + pluginManager plugins.Manager } // ApplicationInsightsQuery is the model that holds the information @@ -210,8 +210,8 @@ func (e *ApplicationInsightsDatasource) executeQuery(ctx context.Context, query func (e *ApplicationInsightsDatasource) createRequest(ctx context.Context, dsInfo *models.DataSource) (*http.Request, error) { // find plugin - plugin, ok := manager.DataSources[dsInfo.Type] - if !ok { + plugin := e.pluginManager.GetDataSource(dsInfo.Type) + if plugin == nil { return nil, errors.New("unable to find datasource plugin Azure Application Insights") } diff --git a/pkg/tsdb/azuremonitor/azure-log-analytics-datasource.go b/pkg/tsdb/azuremonitor/azure-log-analytics-datasource.go index ccce0ee68ad..8a1439fdcaf 100644 --- a/pkg/tsdb/azuremonitor/azure-log-analytics-datasource.go +++ b/pkg/tsdb/azuremonitor/azure-log-analytics-datasource.go @@ -17,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util/errutil" "github.com/opentracing/opentracing-go" @@ -26,8 +25,9 @@ import ( // AzureLogAnalyticsDatasource calls the Azure Log Analytics API's type AzureLogAnalyticsDatasource struct { - httpClient *http.Client - dsInfo *models.DataSource + httpClient *http.Client + dsInfo *models.DataSource + pluginManager plugins.Manager } // AzureLogAnalyticsQuery is the query request that is built from the saved values for @@ -217,8 +217,8 @@ func (e *AzureLogAnalyticsDatasource) createRequest(ctx context.Context, dsInfo req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion)) // find plugin - plugin, ok := manager.DataSources[dsInfo.Type] - if !ok { + plugin := e.pluginManager.GetDataSource(dsInfo.Type) + if plugin == nil { return nil, errors.New("unable to find datasource plugin Azure Monitor") } cloudName := dsInfo.JsonData.Get("cloudName").MustString("azuremonitor") diff --git a/pkg/tsdb/azuremonitor/azuremonitor-datasource.go b/pkg/tsdb/azuremonitor/azuremonitor-datasource.go index ce72904ce4b..76b871b9261 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor-datasource.go +++ b/pkg/tsdb/azuremonitor/azuremonitor-datasource.go @@ -17,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/api/pluginproxy" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util/errutil" opentracing "github.com/opentracing/opentracing-go" @@ -26,8 +25,9 @@ import ( // AzureMonitorDatasource calls the Azure Monitor API - one of the four API's supported type AzureMonitorDatasource struct { - httpClient *http.Client - dsInfo *models.DataSource + httpClient *http.Client + dsInfo *models.DataSource + pluginManager plugins.Manager } var ( @@ -226,8 +226,8 @@ func (e *AzureMonitorDatasource) executeQuery(ctx context.Context, query *AzureM func (e *AzureMonitorDatasource) createRequest(ctx context.Context, dsInfo *models.DataSource) (*http.Request, error) { // find plugin - plugin, ok := manager.DataSources[dsInfo.Type] - if !ok { + plugin := e.pluginManager.GetDataSource(dsInfo.Type) + if plugin == nil { return nil, errors.New("unable to find datasource plugin Azure Monitor") } diff --git a/pkg/tsdb/azuremonitor/azuremonitor.go b/pkg/tsdb/azuremonitor/azuremonitor.go index 0d2437d151e..c1ecff1fea6 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor.go +++ b/pkg/tsdb/azuremonitor/azuremonitor.go @@ -26,6 +26,7 @@ func init() { } type Service struct { + PluginManager plugins.Manager `inject:""` } func (s *Service) Init() error { @@ -34,8 +35,9 @@ func (s *Service) Init() error { // AzureMonitorExecutor executes queries for the Azure Monitor datasource - all four services type AzureMonitorExecutor struct { - httpClient *http.Client - dsInfo *models.DataSource + httpClient *http.Client + dsInfo *models.DataSource + pluginManager plugins.Manager } // NewAzureMonitorExecutor initializes a http client @@ -46,8 +48,9 @@ func (s *Service) NewExecutor(dsInfo *models.DataSource) (plugins.DataPlugin, er } return &AzureMonitorExecutor{ - httpClient: httpClient, - dsInfo: dsInfo, + httpClient: httpClient, + dsInfo: dsInfo, + pluginManager: s.PluginManager, }, nil } @@ -82,23 +85,27 @@ func (e *AzureMonitorExecutor) DataQuery(ctx context.Context, dsInfo *models.Dat } azDatasource := &AzureMonitorDatasource{ - httpClient: e.httpClient, - dsInfo: e.dsInfo, + httpClient: e.httpClient, + dsInfo: e.dsInfo, + pluginManager: e.pluginManager, } aiDatasource := &ApplicationInsightsDatasource{ - httpClient: e.httpClient, - dsInfo: e.dsInfo, + httpClient: e.httpClient, + dsInfo: e.dsInfo, + pluginManager: e.pluginManager, } alaDatasource := &AzureLogAnalyticsDatasource{ - httpClient: e.httpClient, - dsInfo: e.dsInfo, + httpClient: e.httpClient, + dsInfo: e.dsInfo, + pluginManager: e.pluginManager, } iaDatasource := &InsightsAnalyticsDatasource{ - httpClient: e.httpClient, - dsInfo: e.dsInfo, + httpClient: e.httpClient, + dsInfo: e.dsInfo, + pluginManager: e.pluginManager, } azResult, err := azDatasource.executeTimeSeriesQuery(ctx, azureMonitorQueries, *tsdbQuery.TimeRange) diff --git a/pkg/tsdb/azuremonitor/insights-analytics-datasource.go b/pkg/tsdb/azuremonitor/insights-analytics-datasource.go index b639aacf777..dc49562fc3b 100644 --- a/pkg/tsdb/azuremonitor/insights-analytics-datasource.go +++ b/pkg/tsdb/azuremonitor/insights-analytics-datasource.go @@ -15,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/api/pluginproxy" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util/errutil" "github.com/opentracing/opentracing-go" @@ -23,8 +22,9 @@ import ( ) type InsightsAnalyticsDatasource struct { - httpClient *http.Client - dsInfo *models.DataSource + httpClient *http.Client + dsInfo *models.DataSource + pluginManager plugins.Manager } type InsightsAnalyticsQuery struct { @@ -187,8 +187,8 @@ func (e *InsightsAnalyticsDatasource) executeQuery(ctx context.Context, query *I func (e *InsightsAnalyticsDatasource) createRequest(ctx context.Context, dsInfo *models.DataSource) (*http.Request, error) { // find plugin - plugin, ok := manager.DataSources[dsInfo.Type] - if !ok { + plugin := e.pluginManager.GetDataSource(dsInfo.Type) + if plugin == nil { return nil, errors.New("unable to find datasource plugin Azure Application Insights") } diff --git a/pkg/tsdb/cloudmonitoring/cloudmonitoring.go b/pkg/tsdb/cloudmonitoring/cloudmonitoring.go index 16cb99cc35b..1a1008e032c 100644 --- a/pkg/tsdb/cloudmonitoring/cloudmonitoring.go +++ b/pkg/tsdb/cloudmonitoring/cloudmonitoring.go @@ -17,7 +17,6 @@ import ( "time" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana-plugin-sdk-go/data" @@ -73,6 +72,7 @@ func init() { } type Service struct { + PluginManager plugins.Manager `inject:""` } func (s *Service) Init() error { @@ -81,8 +81,9 @@ func (s *Service) Init() error { // Executor executes queries for the CloudMonitoring datasource. type Executor struct { - httpClient *http.Client - dsInfo *models.DataSource + httpClient *http.Client + dsInfo *models.DataSource + pluginManager plugins.Manager } // NewExecutor returns an Executor. @@ -93,8 +94,9 @@ func (s *Service) NewExecutor(dsInfo *models.DataSource) (plugins.DataPlugin, er } return &Executor{ - httpClient: httpClient, - dsInfo: dsInfo, + httpClient: httpClient, + dsInfo: dsInfo, + pluginManager: s.PluginManager, }, nil } @@ -534,8 +536,8 @@ func (e *Executor) createRequest(ctx context.Context, dsInfo *models.DataSource, req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion)) // find plugin - plugin, ok := manager.DataSources[dsInfo.Type] - if !ok { + plugin := e.pluginManager.GetDataSource(dsInfo.Type) + if plugin == nil { return nil, errors.New("unable to find datasource plugin CloudMonitoring") } diff --git a/pkg/tsdb/service.go b/pkg/tsdb/service.go index 4195def1fd3..d89e8ca459b 100644 --- a/pkg/tsdb/service.go +++ b/pkg/tsdb/service.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/azuremonitor" @@ -46,7 +45,7 @@ type Service struct { PostgresService *postgres.PostgresService `inject:""` CloudMonitoringService *cloudmonitoring.Service `inject:""` AzureMonitorService *azuremonitor.Service `inject:""` - PluginManager *manager.PluginManager `inject:""` + PluginManager plugins.Manager `inject:""` registry map[string]func(*models.DataSource) (plugins.DataPlugin, error) }