From eacee08135cb7dc040c9ddfeb8e5cae18399e6e8 Mon Sep 17 00:00:00 2001 From: Jeff Levin Date: Wed, 6 Jul 2022 15:51:44 -0800 Subject: [PATCH] public dashboards: move into into its own service (#51358) This PR moves public dashboards into its own self contained service including API, Service, Database, and Models. Routes are mounted on the Grafana HTTPServer by the API service at injection time with wire.go. The main route that loads the frontend for public dashboards is still handled by the API package. Co-authored-by: Jesse Weaver Co-authored-by: Owen Smallwood --- pkg/api/api.go | 19 +- pkg/api/common_test.go | 10 - pkg/api/dashboard_public.go | 141 ---- pkg/api/dashboard_public_test.go | 630 ------------------ pkg/api/http_server.go | 6 +- pkg/server/wire.go | 9 + pkg/services/dashboards/dashboard.go | 11 - .../dashboards/dashboard_service_mock.go | 119 +--- pkg/services/dashboards/models.go | 7 - pkg/services/dashboards/store_mock.go | 115 +--- pkg/services/publicdashboards/api/api.go | 234 +++++++ pkg/services/publicdashboards/api/api_test.go | 589 ++++++++++++++++ .../publicdashboards/api/common_test.go | 174 +++++ .../publicdashboards/api/middleware.go} | 6 +- .../database/database.go} | 57 +- .../database/database_test.go} | 134 ++-- .../publicdashboards/models/models.go} | 53 +- .../publicdashboards/models/models_test.go} | 9 +- .../public_dashboard_service_mock.go | 144 ++++ .../public_dashboard_store_mock.go | 142 ++++ .../publicdashboards/publicdashboard.go | 29 + .../service/service.go} | 75 ++- .../service/service_test.go} | 117 ++-- 23 files changed, 1632 insertions(+), 1198 deletions(-) delete mode 100644 pkg/api/dashboard_public.go delete mode 100644 pkg/api/dashboard_public_test.go create mode 100644 pkg/services/publicdashboards/api/api.go create mode 100644 pkg/services/publicdashboards/api/api_test.go create mode 100644 pkg/services/publicdashboards/api/common_test.go rename pkg/{middleware/dashboard_public.go => services/publicdashboards/api/middleware.go} (65%) rename pkg/services/{dashboards/database/database_dashboard_public.go => publicdashboards/database/database.go} (53%) rename pkg/services/{dashboards/database/database_dashboard_public_test.go => publicdashboards/database/database_test.go} (62%) rename pkg/{models/dashboards_public.go => services/publicdashboards/models/models.go} (54%) rename pkg/{models/dashboards_public_test.go => services/publicdashboards/models/models_test.go} (84%) create mode 100644 pkg/services/publicdashboards/public_dashboard_service_mock.go create mode 100644 pkg/services/publicdashboards/public_dashboard_store_mock.go create mode 100644 pkg/services/publicdashboards/publicdashboard.go rename pkg/services/{dashboards/service/dashboard_public.go => publicdashboards/service/service.go} (55%) rename pkg/services/{dashboards/service/dashboard_public_test.go => publicdashboards/service/service_test.go} (81%) diff --git a/pkg/api/api.go b/pkg/api/api.go index fbe8e735e97..df4aaf68295 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" + publicdashboardsapi "github.com/grafana/grafana/pkg/services/publicdashboards/api" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/web" ) @@ -103,6 +104,10 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/dashboards/*", reqSignedIn, hs.Index) r.Get("/goto/:uid", reqSignedIn, hs.redirectFromShortURL, hs.Index) + if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) { + r.Get("/public-dashboards/:accessToken", publicdashboardsapi.SetPublicDashboardFlag(), hs.Index) + } + r.Get("/explore", authorize(func(c *models.ReqContext) { if f, ok := reqSignedIn.(func(c *models.ReqContext)); ok { f(c) @@ -391,11 +396,6 @@ func (hs *HTTPServer) registerRoutes() { }) dashboardRoute.Group("/uid/:uid", func(dashUidRoute routing.RouteRegister) { - if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) { - dashUidRoute.Get("/public-config", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.GetPublicDashboardConfig)) - dashUidRoute.Post("/public-config", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.SavePublicDashboardConfig)) - } - if hs.ThumbService != nil { dashUidRoute.Get("/img/:kind/:theme", hs.ThumbService.GetImage) if hs.Features.IsEnabled(featuremgmt.FlagDashboardPreviewsAdmin) { @@ -598,7 +598,7 @@ func (hs *HTTPServer) registerRoutes() { // grafana.net proxy r.Any("/api/gnet/*", reqSignedIn, hs.ProxyGnetRequest) - // Gravatar service. + // Gravatar service r.Get("/avatar/:hash", hs.AvatarCacheServer.Handler) // Snapshots @@ -608,13 +608,6 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/api/snapshots-delete/:deleteKey", reqSnapshotPublicModeOrSignedIn, routing.Wrap(hs.DeleteDashboardSnapshotByDeleteKey)) r.Delete("/api/snapshots/:key", reqEditorRole, routing.Wrap(hs.DeleteDashboardSnapshot)) - // Public API - if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) { - r.Get("/public-dashboards/:accessToken", middleware.SetPublicDashboardFlag(), hs.Index) - r.Get("/api/public/dashboards/:accessToken", routing.Wrap(hs.GetPublicDashboard)) - r.Post("/api/public/dashboards/:accessToken/panels/:panelId/query", routing.Wrap(hs.QueryPublicDashboard)) - } - // Frontend logs sourceMapStore := frontendlogging.NewSourceMapStore(hs.Cfg, hs.pluginStaticRouteResolver, frontendlogging.ReadSourceMapFromFS) r.Post("/log", middleware.RateLimit(hs.Cfg.Sentry.EndpointRPS, hs.Cfg.Sentry.EndpointBurst, time.Now), diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index b5b061a4942..3111965b170 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -43,7 +43,6 @@ import ( "github.com/grafana/grafana/pkg/services/searchusers" "github.com/grafana/grafana/pkg/services/searchusers/filters" "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web/webtest" @@ -341,15 +340,6 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont return setupHTTPServerWithCfgDb(t, useFakeAccessControl, enableAccessControl, cfg, db, db, featuremgmt.WithFeatures()) } -func setupHTTPServerWithMockDb(t *testing.T, useFakeAccessControl, enableAccessControl bool, features *featuremgmt.FeatureManager) accessControlScenarioContext { - // Use a new conf - cfg := setting.NewCfg() - db := sqlstore.InitTestDB(t) - db.Cfg = setting.NewCfg() - - return setupHTTPServerWithCfgDb(t, useFakeAccessControl, enableAccessControl, cfg, db, mockstore.NewSQLStoreMock(), features) -} - func setupHTTPServerWithCfgDb(t *testing.T, useFakeAccessControl, enableAccessControl bool, cfg *setting.Cfg, db *sqlstore.SQLStore, store sqlstore.Store, features *featuremgmt.FeatureManager) accessControlScenarioContext { t.Helper() diff --git a/pkg/api/dashboard_public.go b/pkg/api/dashboard_public.go deleted file mode 100644 index b23108c3725..00000000000 --- a/pkg/api/dashboard_public.go +++ /dev/null @@ -1,141 +0,0 @@ -package api - -import ( - "errors" - "fmt" - "net/http" - "strconv" - "strings" - - "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/web" -) - -// gets public dashboard -func (hs *HTTPServer) GetPublicDashboard(c *models.ReqContext) response.Response { - accessToken := web.Params(c.Req)[":accessToken"] - - dash, err := hs.DashboardService.GetPublicDashboard(c.Req.Context(), accessToken) - if err != nil { - return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard", err) - } - - meta := dtos.DashboardMeta{ - Slug: dash.Slug, - Type: models.DashTypeDB, - CanStar: false, - CanSave: false, - CanEdit: false, - CanAdmin: false, - CanDelete: false, - Created: dash.Created, - Updated: dash.Updated, - Version: dash.Version, - IsFolder: false, - FolderId: dash.FolderId, - PublicDashboardAccessToken: accessToken, - } - - dto := dtos.DashboardFullWithMeta{Meta: meta, Dashboard: dash.Data} - - return response.JSON(http.StatusOK, dto) -} - -// gets public dashboard configuration for dashboard -func (hs *HTTPServer) GetPublicDashboardConfig(c *models.ReqContext) response.Response { - pdc, err := hs.DashboardService.GetPublicDashboardConfig(c.Req.Context(), c.OrgId, web.Params(c.Req)[":uid"]) - if err != nil { - return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard config", err) - } - return response.JSON(http.StatusOK, pdc) -} - -// sets public dashboard configuration for dashboard -func (hs *HTTPServer) SavePublicDashboardConfig(c *models.ReqContext) response.Response { - pubdash := &models.PublicDashboard{} - if err := web.Bind(c.Req, pubdash); err != nil { - return response.Error(http.StatusBadRequest, "bad request data", err) - } - - // Always set the org id to the current auth session orgId - pubdash.OrgId = c.OrgId - - dto := dashboards.SavePublicDashboardConfigDTO{ - OrgId: c.OrgId, - DashboardUid: web.Params(c.Req)[":uid"], - UserId: c.UserId, - PublicDashboard: pubdash, - } - - pubdash, err := hs.DashboardService.SavePublicDashboardConfig(c.Req.Context(), &dto) - if err != nil { - return handleDashboardErr(http.StatusInternalServerError, "Failed to save public dashboard configuration", err) - } - - return response.JSON(http.StatusOK, pubdash) -} - -// QueryPublicDashboard returns all results for a given panel on a public dashboard -// POST /api/public/dashboard/:accessToken/panels/:panelId/query -func (hs *HTTPServer) QueryPublicDashboard(c *models.ReqContext) response.Response { - panelId, err := strconv.ParseInt(web.Params(c.Req)[":panelId"], 10, 64) - if err != nil { - return response.Error(http.StatusBadRequest, "invalid panel ID", err) - } - - dashboard, err := hs.DashboardService.GetPublicDashboard(c.Req.Context(), web.Params(c.Req)[":accessToken"]) - if err != nil { - return response.Error(http.StatusInternalServerError, "could not fetch dashboard", err) - } - - publicDashboard, err := hs.DashboardService.GetPublicDashboardConfig(c.Req.Context(), dashboard.OrgId, dashboard.Uid) - if err != nil { - return response.Error(http.StatusInternalServerError, "could not fetch public dashboard", err) - } - - reqDTO, err := hs.DashboardService.BuildPublicDashboardMetricRequest( - c.Req.Context(), - dashboard, - publicDashboard, - panelId, - ) - if err != nil { - return handleDashboardErr(http.StatusInternalServerError, "Failed to get queries for public dashboard", err) - } - - // Get all needed datasource UIDs from queries - var uids []string - for _, query := range reqDTO.Queries { - uids = append(uids, query.Get("datasource").Get("uid").MustString()) - } - - // Create a temp user with read-only datasource permissions - anonymousUser := &models.SignedInUser{OrgId: dashboard.OrgId, Permissions: make(map[int64]map[string][]string)} - permissions := make(map[string][]string) - datasourceScope := fmt.Sprintf("datasources:uid:%s", strings.Join(uids, ",")) - permissions[datasources.ActionQuery] = []string{datasourceScope} - permissions[datasources.ActionRead] = []string{datasourceScope} - anonymousUser.Permissions[dashboard.OrgId] = permissions - - resp, err := hs.queryDataService.QueryDataMultipleSources(c.Req.Context(), anonymousUser, c.SkipCache, reqDTO, true) - - if err != nil { - return hs.handleQueryMetricsError(err) - } - return hs.toJsonStreamingResponse(resp) -} - -// util to help us unpack a dashboard err or use default http code and message -func handleDashboardErr(defaultCode int, defaultMsg string, err error) response.Response { - var dashboardErr dashboards.DashboardErr - - if ok := errors.As(err, &dashboardErr); ok { - return response.Error(dashboardErr.StatusCode, dashboardErr.Error(), dashboardErr) - } - - return response.Error(defaultCode, defaultMsg, err) -} diff --git a/pkg/api/dashboard_public_test.go b/pkg/api/dashboard_public_test.go deleted file mode 100644 index 9ea6360a038..00000000000 --- a/pkg/api/dashboard_public_test.go +++ /dev/null @@ -1,630 +0,0 @@ -package api - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "net/http" - "strings" - "testing" - - "github.com/google/uuid" - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/infra/localcache" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/datasources" - fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes" - "github.com/grafana/grafana/pkg/services/datasources/service" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/query" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/web/webtest" -) - -func TestAPIGetPublicDashboard(t *testing.T) { - t.Run("It should 404 if featureflag is not enabled", func(t *testing.T) { - sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures()) - dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")). - Return(&models.Dashboard{}, nil).Maybe() - sc.hs.DashboardService = dashSvc - - setInitCtxSignedInViewer(sc.initCtx) - response := callAPI( - sc.server, - http.MethodGet, - "/api/public/dashboards", - nil, - t, - ) - assert.Equal(t, http.StatusNotFound, response.Code) - response = callAPI( - sc.server, - http.MethodGet, - "/api/public/dashboards/asdf", - nil, - t, - ) - assert.Equal(t, http.StatusNotFound, response.Code) - }) - - DashboardUid := "dashboard-abcd1234" - token, err := uuid.NewRandom() - require.NoError(t, err) - accessToken := fmt.Sprintf("%x", token) - - testCases := []struct { - Name string - AccessToken string - ExpectedHttpResponse int - publicDashboardResult *models.Dashboard - publicDashboardErr error - }{ - { - Name: "It gets a public dashboard", - AccessToken: accessToken, - ExpectedHttpResponse: http.StatusOK, - publicDashboardResult: &models.Dashboard{ - Data: simplejson.NewFromAny(map[string]interface{}{ - "Uid": DashboardUid, - }), - }, - publicDashboardErr: nil, - }, - { - Name: "It should return 404 if isPublicDashboard is false", - AccessToken: accessToken, - ExpectedHttpResponse: http.StatusNotFound, - publicDashboardResult: nil, - publicDashboardErr: dashboards.ErrPublicDashboardNotFound, - }, - } - - for _, test := range testCases { - t.Run(test.Name, func(t *testing.T) { - sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)) - dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")). - Return(test.publicDashboardResult, test.publicDashboardErr) - sc.hs.DashboardService = dashSvc - - setInitCtxSignedInViewer(sc.initCtx) - response := callAPI( - sc.server, - http.MethodGet, - fmt.Sprintf("/api/public/dashboards/%s", test.AccessToken), - nil, - t, - ) - - assert.Equal(t, test.ExpectedHttpResponse, response.Code) - - if test.publicDashboardErr == nil { - var dashResp dtos.DashboardFullWithMeta - err := json.Unmarshal(response.Body.Bytes(), &dashResp) - require.NoError(t, err) - - assert.Equal(t, DashboardUid, dashResp.Dashboard.Get("Uid").MustString()) - assert.Equal(t, false, dashResp.Meta.CanEdit) - assert.Equal(t, false, dashResp.Meta.CanDelete) - assert.Equal(t, false, dashResp.Meta.CanSave) - } else { - var errResp struct { - Error string `json:"error"` - } - err := json.Unmarshal(response.Body.Bytes(), &errResp) - require.NoError(t, err) - assert.Equal(t, test.publicDashboardErr.Error(), errResp.Error) - } - }) - } -} - -func TestAPIGetPublicDashboardConfig(t *testing.T) { - pubdash := &models.PublicDashboard{IsEnabled: true} - - testCases := []struct { - Name string - DashboardUid string - ExpectedHttpResponse int - PublicDashboardResult *models.PublicDashboard - PublicDashboardError error - }{ - { - Name: "retrieves public dashboard config when dashboard is found", - DashboardUid: "1", - ExpectedHttpResponse: http.StatusOK, - PublicDashboardResult: pubdash, - PublicDashboardError: nil, - }, - { - Name: "returns 404 when dashboard not found", - DashboardUid: "77777", - ExpectedHttpResponse: http.StatusNotFound, - PublicDashboardResult: nil, - PublicDashboardError: dashboards.ErrDashboardNotFound, - }, - { - Name: "returns 500 when internal server error", - DashboardUid: "1", - ExpectedHttpResponse: http.StatusInternalServerError, - PublicDashboardResult: nil, - PublicDashboardError: errors.New("database broken"), - }, - } - - for _, test := range testCases { - t.Run(test.Name, func(t *testing.T) { - sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)) - dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetPublicDashboardConfig", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")). - Return(test.PublicDashboardResult, test.PublicDashboardError) - sc.hs.DashboardService = dashSvc - - setInitCtxSignedInViewer(sc.initCtx) - response := callAPI( - sc.server, - http.MethodGet, - "/api/dashboards/uid/1/public-config", - nil, - t, - ) - - assert.Equal(t, test.ExpectedHttpResponse, response.Code) - - if response.Code == http.StatusOK { - var pdcResp models.PublicDashboard - err := json.Unmarshal(response.Body.Bytes(), &pdcResp) - require.NoError(t, err) - assert.Equal(t, test.PublicDashboardResult, &pdcResp) - } - }) - } -} - -func TestApiSavePublicDashboardConfig(t *testing.T) { - testCases := []struct { - Name string - DashboardUid string - publicDashboardConfig *models.PublicDashboard - ExpectedHttpResponse int - saveDashboardError error - }{ - { - Name: "returns 200 when update persists", - DashboardUid: "1", - publicDashboardConfig: &models.PublicDashboard{IsEnabled: true}, - ExpectedHttpResponse: http.StatusOK, - saveDashboardError: nil, - }, - { - Name: "returns 500 when not persisted", - ExpectedHttpResponse: http.StatusInternalServerError, - publicDashboardConfig: &models.PublicDashboard{}, - saveDashboardError: errors.New("backend failed to save"), - }, - { - Name: "returns 404 when dashboard not found", - ExpectedHttpResponse: http.StatusNotFound, - publicDashboardConfig: &models.PublicDashboard{}, - saveDashboardError: dashboards.ErrDashboardNotFound, - }, - } - - for _, test := range testCases { - t.Run(test.Name, func(t *testing.T) { - sc := setupHTTPServerWithMockDb(t, false, false, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)) - - dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("SavePublicDashboardConfig", mock.Anything, mock.AnythingOfType("*dashboards.SavePublicDashboardConfigDTO")). - Return(&models.PublicDashboard{IsEnabled: true}, test.saveDashboardError) - sc.hs.DashboardService = dashSvc - - setInitCtxSignedInViewer(sc.initCtx) - response := callAPI( - sc.server, - http.MethodPost, - "/api/dashboards/uid/1/public-config", - strings.NewReader(`{ "isPublic": true }`), - t, - ) - - assert.Equal(t, test.ExpectedHttpResponse, response.Code) - - // check the result if it's a 200 - if response.Code == http.StatusOK { - val, err := json.Marshal(test.publicDashboardConfig) - require.NoError(t, err) - assert.Equal(t, string(val), response.Body.String()) - } - }) - } -} - -// `/public/dashboards/:uid/query`` endpoint test -func TestAPIQueryPublicDashboard(t *testing.T) { - queryReturnsError := false - - qds := query.ProvideService( - nil, - &fakeDatasources.FakeCacheService{ - DataSources: []*datasources.DataSource{ - {Uid: "mysqlds"}, - {Uid: "promds"}, - {Uid: "promds2"}, - }, - }, - nil, - &fakePluginRequestValidator{}, - &fakeDatasources.FakeDataSourceService{}, - &fakePluginClient{ - QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - if queryReturnsError { - return nil, errors.New("error") - } - - resp := backend.Responses{} - - for _, query := range req.Queries { - resp[query.RefID] = backend.DataResponse{ - Frames: []*data.Frame{ - { - RefID: query.RefID, - Name: "query-" + query.RefID, - }, - }, - } - } - return &backend.QueryDataResponse{Responses: resp}, nil - }, - }, - &fakeOAuthTokenService{}, - ) - - setup := func(enabled bool) (*webtest.Server, *dashboards.FakeDashboardService) { - fakeDashboardService := &dashboards.FakeDashboardService{} - - return SetupAPITestServer(t, func(hs *HTTPServer) { - hs.queryDataService = qds - hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards, enabled) - hs.DashboardService = fakeDashboardService - }), fakeDashboardService - } - - t.Run("Status code is 404 when feature toggle is disabled", func(t *testing.T) { - server, _ := setup(false) - - req := server.NewPostRequest( - "/api/public/dashboards/abc123/panels/2/query", - strings.NewReader("{}"), - ) - resp, err := server.SendJSON(req) - require.NoError(t, err) - require.NoError(t, resp.Body.Close()) - require.Equal(t, http.StatusNotFound, resp.StatusCode) - }) - - t.Run("Status code is 400 when the panel ID is invalid", func(t *testing.T) { - server, _ := setup(true) - - req := server.NewPostRequest( - "/api/public/dashboards/abc123/panels/notanumber/query", - strings.NewReader("{}"), - ) - resp, err := server.SendJSON(req) - require.NoError(t, err) - require.NoError(t, resp.Body.Close()) - require.Equal(t, http.StatusBadRequest, resp.StatusCode) - }) - - t.Run("Returns query data when feature toggle is enabled", func(t *testing.T) { - server, fakeDashboardService := setup(true) - - fakeDashboardService.On("GetPublicDashboard", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil) - fakeDashboardService.On("GetPublicDashboardConfig", mock.Anything, mock.Anything, mock.Anything).Return(&models.PublicDashboard{}, nil) - - fakeDashboardService.On( - "BuildPublicDashboardMetricRequest", - mock.Anything, - mock.Anything, - mock.Anything, - int64(2), - ).Return(dtos.MetricRequest{ - Queries: []*simplejson.Json{ - simplejson.MustJson([]byte(` - { - "datasource": { - "type": "prometheus", - "uid": "promds" - }, - "exemplar": true, - "expr": "query_2_A", - "interval": "", - "legendFormat": "", - "refId": "A" - } - `)), - }, - }, nil) - req := server.NewPostRequest( - "/api/public/dashboards/abc123/panels/2/query", - strings.NewReader("{}"), - ) - resp, err := server.SendJSON(req) - require.NoError(t, err) - bodyBytes, err := ioutil.ReadAll(resp.Body) - require.NoError(t, err) - require.JSONEq( - t, - `{ - "results": { - "A": { - "frames": [ - { - "data": { - "values": [] - }, - "schema": { - "fields": [], - "refId": "A", - "name": "query-A" - } - } - ] - } - } - }`, - string(bodyBytes), - ) - require.NoError(t, resp.Body.Close()) - require.Equal(t, http.StatusOK, resp.StatusCode) - }) - - t.Run("Status code is 500 when the query fails", func(t *testing.T) { - server, fakeDashboardService := setup(true) - - fakeDashboardService.On("GetPublicDashboard", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil) - fakeDashboardService.On("GetPublicDashboardConfig", mock.Anything, mock.Anything, mock.Anything).Return(&models.PublicDashboard{}, nil) - fakeDashboardService.On( - "BuildPublicDashboardMetricRequest", - mock.Anything, - mock.Anything, - mock.Anything, - int64(2), - ).Return(dtos.MetricRequest{ - Queries: []*simplejson.Json{ - simplejson.MustJson([]byte(` - { - "datasource": { - "type": "prometheus", - "uid": "promds" - }, - "exemplar": true, - "expr": "query_2_A", - "interval": "", - "legendFormat": "", - "refId": "A" - } - `)), - }, - }, nil) - req := server.NewPostRequest( - "/api/public/dashboards/abc123/panels/2/query", - strings.NewReader("{}"), - ) - queryReturnsError = true - resp, err := server.SendJSON(req) - require.NoError(t, err) - require.NoError(t, resp.Body.Close()) - require.Equal(t, http.StatusInternalServerError, resp.StatusCode) - queryReturnsError = false - }) - - t.Run("Status code is 200 when a panel has queries from multiple datasources", func(t *testing.T) { - server, fakeDashboardService := setup(true) - - fakeDashboardService.On("GetPublicDashboard", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil) - fakeDashboardService.On("GetPublicDashboardConfig", mock.Anything, mock.Anything, mock.Anything).Return(&models.PublicDashboard{}, nil) - fakeDashboardService.On( - "BuildPublicDashboardMetricRequest", - mock.Anything, - mock.Anything, - mock.Anything, - int64(2), - ).Return(dtos.MetricRequest{ - Queries: []*simplejson.Json{ - simplejson.MustJson([]byte(` - { - "datasource": { - "type": "prometheus", - "uid": "promds" - }, - "exemplar": true, - "expr": "query_2_A", - "interval": "", - "legendFormat": "", - "refId": "A" - } - `)), - simplejson.MustJson([]byte(` - { - "datasource": { - "type": "prometheus", - "uid": "promds2" - }, - "exemplar": true, - "expr": "query_2_B", - "interval": "", - "legendFormat": "", - "refId": "B" - } - `)), - }, - }, nil) - req := server.NewPostRequest( - "/api/public/dashboards/abc123/panels/2/query", - strings.NewReader("{}"), - ) - resp, err := server.SendJSON(req) - require.NoError(t, err) - bodyBytes, err := ioutil.ReadAll(resp.Body) - require.NoError(t, err) - require.JSONEq( - t, - `{ - "results": { - "A": { - "frames": [ - { - "data": { - "values": [] - }, - "schema": { - "fields": [], - "refId": "A", - "name": "query-A" - } - } - ] - }, - "B": { - "frames": [ - { - "data": { - "values": [] - }, - "schema": { - "fields": [], - "refId": "B", - "name": "query-B" - } - } - ] - } - } - }`, - string(bodyBytes), - ) - require.NoError(t, resp.Body.Close()) - require.Equal(t, http.StatusOK, resp.StatusCode) - }) -} - -func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) { - config := setting.NewCfg() - db := sqlstore.InitTestDB(t) - scenario := setupHTTPServerWithCfgDb(t, false, false, config, db, db, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)) - scenario.initCtx.SkipCache = true - cacheService := service.ProvideCacheService(localcache.ProvideService(), db) - qds := query.ProvideService( - nil, - cacheService, - nil, - &fakePluginRequestValidator{}, - &fakeDatasources.FakeDataSourceService{}, - &fakePluginClient{ - QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - resp := backend.Responses{ - "A": backend.DataResponse{ - Frames: []*data.Frame{{}}, - }, - } - return &backend.QueryDataResponse{Responses: resp}, nil - }, - }, - &fakeOAuthTokenService{}, - ) - scenario.hs.queryDataService = qds - - _ = db.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{ - Uid: "ds1", - OrgId: 1, - Name: "laban", - Type: datasources.DS_MYSQL, - Access: datasources.DS_ACCESS_DIRECT, - Url: "http://test", - Database: "site", - ReadOnly: true, - }) - - // Create Dashboard - saveDashboardCmd := models.SaveDashboardCommand{ - OrgId: 1, - FolderId: 1, - IsFolder: false, - Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": nil, - "title": "test", - "panels": []map[string]interface{}{ - { - "id": 1, - "targets": []map[string]interface{}{ - { - "datasource": map[string]string{ - "type": "mysql", - "uid": "ds1", - }, - "refId": "A", - }, - }, - }, - }, - }), - } - dashboard, _ := scenario.dashboardsStore.SaveDashboard(saveDashboardCmd) - - // Create public dashboard - savePubDashboardCmd := &dashboards.SavePublicDashboardConfigDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, - PublicDashboard: &models.PublicDashboard{ - IsEnabled: true, - }, - } - - pubdash, err := scenario.hs.DashboardService.SavePublicDashboardConfig(context.Background(), savePubDashboardCmd) - require.NoError(t, err) - - response := callAPI( - scenario.server, - http.MethodPost, - fmt.Sprintf("/api/public/dashboards/%s/panels/1/query", pubdash.AccessToken), - strings.NewReader(`{}`), - t, - ) - - require.Equal(t, http.StatusOK, response.Code) - bodyBytes, err := ioutil.ReadAll(response.Body) - require.NoError(t, err) - require.JSONEq( - t, - `{ - "results": { - "A": { - "frames": [ - { - "data": { - "values": [] - }, - "schema": { - "fields": [] - } - } - ] - } - } - }`, - string(bodyBytes), - ) -} diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index db9438ca4a2..ac80801de41 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -62,6 +62,8 @@ import ( pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings/service" pref "github.com/grafana/grafana/pkg/services/preference" "github.com/grafana/grafana/pkg/services/provisioning" + + publicdashboardsApi "github.com/grafana/grafana/pkg/services/publicdashboards/api" "github.com/grafana/grafana/pkg/services/query" "github.com/grafana/grafana/pkg/services/queryhistory" "github.com/grafana/grafana/pkg/services/quota" @@ -163,6 +165,7 @@ type HTTPServer struct { folderPermissionsService accesscontrol.FolderPermissionsService dashboardPermissionsService accesscontrol.DashboardPermissionsService dashboardVersionService dashver.Service + PublicDashboardsApi *publicdashboardsApi.Api starService star.Service CoremodelRegistry *registry.Generic CoremodelStaticRegistry *registry.Static @@ -202,7 +205,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi teamsPermissionsService accesscontrol.TeamPermissionsService, folderPermissionsService accesscontrol.FolderPermissionsService, dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service, starService star.Service, csrfService csrf.Service, coremodelRegistry *registry.Generic, coremodelStaticRegistry *registry.Static, - kvStore kvstore.KVStore, secretsMigrator secrets.Migrator, remoteSecretsCheck secretsKV.UseRemoteSecretsPluginCheck, + kvStore kvstore.KVStore, secretsMigrator secrets.Migrator, remoteSecretsCheck secretsKV.UseRemoteSecretsPluginCheck, publicDashboardsApi *publicdashboardsApi.Api, ) (*HTTPServer, error) { web.Env = cfg.Env m := web.New() @@ -287,6 +290,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi CoremodelRegistry: coremodelRegistry, CoremodelStaticRegistry: coremodelStaticRegistry, kvStore: kvStore, + PublicDashboardsApi: publicDashboardsApi, secretsMigrator: secretsMigrator, } if hs.Listener != nil { diff --git a/pkg/server/wire.go b/pkg/server/wire.go index c53b0bf19b0..8c3aafa4930 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -77,6 +77,10 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsettings" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings/service" "github.com/grafana/grafana/pkg/services/preference/prefimpl" + "github.com/grafana/grafana/pkg/services/publicdashboards" + publicdashboardsApi "github.com/grafana/grafana/pkg/services/publicdashboards/api" + publicdashboardsStore "github.com/grafana/grafana/pkg/services/publicdashboards/database" + publicdashboardsService "github.com/grafana/grafana/pkg/services/publicdashboards/service" "github.com/grafana/grafana/pkg/services/query" "github.com/grafana/grafana/pkg/services/queryhistory" "github.com/grafana/grafana/pkg/services/quota" @@ -275,6 +279,11 @@ var wireBasicSet = wire.NewSet( wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), starimpl.ProvideService, dashverimpl.ProvideService, + publicdashboardsService.ProvideService, + wire.Bind(new(publicdashboards.Service), new(*publicdashboardsService.PublicDashboardServiceImpl)), + publicdashboardsStore.ProvideStore, + wire.Bind(new(publicdashboards.Store), new(*publicdashboardsStore.PublicDashboardStoreImpl)), + publicdashboardsApi.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, ) diff --git a/pkg/services/dashboards/dashboard.go b/pkg/services/dashboards/dashboard.go index c3cbb277e00..b3a1b3cffe3 100644 --- a/pkg/services/dashboards/dashboard.go +++ b/pkg/services/dashboards/dashboard.go @@ -3,15 +3,12 @@ package dashboards import ( "context" - "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/models" ) //go:generate mockery --name DashboardService --structname FakeDashboardService --inpackage --filename dashboard_service_mock.go // DashboardService is a service for operating on dashboards. type DashboardService interface { - BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *models.PublicDashboard, panelId int64) (dtos.MetricRequest, error) - BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) (*models.SignedInUser, error) BuildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) @@ -20,14 +17,11 @@ type DashboardService interface { GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error GetDashboardTags(ctx context.Context, query *models.GetDashboardTagsQuery) error GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error - GetPublicDashboard(ctx context.Context, accessToken string) (*models.Dashboard, error) - GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *models.HasAdminPermissionInDashboardsOrFoldersQuery) error HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*models.Dashboard, error) MakeUserAdmin(ctx context.Context, orgID int64, userID, dashboardID int64, setViewAndEditPermissions bool) error SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error) - SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*models.PublicDashboard, error) SearchDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) error UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardAcl) error } @@ -66,18 +60,13 @@ type Store interface { GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) GetProvisionedDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) GetProvisionedDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error) - GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) - GetPublicDashboard(ctx context.Context, accessToken string) (*models.PublicDashboard, *models.Dashboard, error) - GenerateNewPublicDashboardUid(ctx context.Context) (string, error) HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *models.HasAdminPermissionInDashboardsOrFoldersQuery) error HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error // SaveAlerts saves dashboard alerts. SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error) SaveProvisionedDashboard(cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) - SavePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboard, error) UnprovisionDashboard(ctx context.Context, id int64) error - UpdatePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) error UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardAcl) error // ValidateDashboardBeforeSave validates a dashboard before save. ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error) diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go index 0c97c70ad24..8e42d531998 100644 --- a/pkg/services/dashboards/dashboard_service_mock.go +++ b/pkg/services/dashboards/dashboard_service_mock.go @@ -1,14 +1,12 @@ -// Code generated by mockery v2.12.1. DO NOT EDIT. +// Code generated by mockery v2.12.2. DO NOT EDIT. package dashboards import ( context "context" - dtos "github.com/grafana/grafana/pkg/api/dtos" - mock "github.com/stretchr/testify/mock" - models "github.com/grafana/grafana/pkg/models" + mock "github.com/stretchr/testify/mock" testing "testing" ) @@ -18,50 +16,6 @@ type FakeDashboardService struct { mock.Mock } -// BuildAnonymousUser provides a mock function with given fields: ctx, dashboard -func (_m *FakeDashboardService) BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) (*models.SignedInUser, error) { - ret := _m.Called(ctx, dashboard) - - var r0 *models.SignedInUser - if rf, ok := ret.Get(0).(func(context.Context, *models.Dashboard) *models.SignedInUser); ok { - r0 = rf(ctx, dashboard) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.SignedInUser) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *models.Dashboard) error); ok { - r1 = rf(ctx, dashboard) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BuildPublicDashboardMetricRequest provides a mock function with given fields: ctx, dashboard, publicDashboard, panelId -func (_m *FakeDashboardService) BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *models.PublicDashboard, panelId int64) (dtos.MetricRequest, error) { - ret := _m.Called(ctx, dashboard, publicDashboard, panelId) - - var r0 dtos.MetricRequest - if rf, ok := ret.Get(0).(func(context.Context, *models.Dashboard, *models.PublicDashboard, int64) dtos.MetricRequest); ok { - r0 = rf(ctx, dashboard, publicDashboard, panelId) - } else { - r0 = ret.Get(0).(dtos.MetricRequest) - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *models.Dashboard, *models.PublicDashboard, int64) error); ok { - r1 = rf(ctx, dashboard, publicDashboard, panelId) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // BuildSaveDashboardCommand provides a mock function with given fields: ctx, dto, shouldValidateAlerts, validateProvisionedDashboard func (_m *FakeDashboardService) BuildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) { ret := _m.Called(ctx, dto, shouldValidateAlerts, validateProvisionedDashboard) @@ -192,52 +146,6 @@ func (_m *FakeDashboardService) GetDashboards(ctx context.Context, query *models return r0 } -// GetPublicDashboard provides a mock function with given fields: ctx, accessToken -func (_m *FakeDashboardService) GetPublicDashboard(ctx context.Context, accessToken string) (*models.Dashboard, error) { - ret := _m.Called(ctx, accessToken) - - var r0 *models.Dashboard - if rf, ok := ret.Get(0).(func(context.Context, string) *models.Dashboard); ok { - r0 = rf(ctx, accessToken) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.Dashboard) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, accessToken) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetPublicDashboardConfig provides a mock function with given fields: ctx, orgId, dashboardUid -func (_m *FakeDashboardService) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) { - ret := _m.Called(ctx, orgId, dashboardUid) - - var r0 *models.PublicDashboard - if rf, ok := ret.Get(0).(func(context.Context, int64, string) *models.PublicDashboard); ok { - r0 = rf(ctx, orgId, dashboardUid) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.PublicDashboard) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok { - r1 = rf(ctx, orgId, dashboardUid) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // HasAdminPermissionInDashboardsOrFolders provides a mock function with given fields: ctx, query func (_m *FakeDashboardService) HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *models.HasAdminPermissionInDashboardsOrFoldersQuery) error { ret := _m.Called(ctx, query) @@ -326,29 +234,6 @@ func (_m *FakeDashboardService) SaveDashboard(ctx context.Context, dto *SaveDash return r0, r1 } -// SavePublicDashboardConfig provides a mock function with given fields: ctx, dto -func (_m *FakeDashboardService) SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*models.PublicDashboard, error) { - ret := _m.Called(ctx, dto) - - var r0 *models.PublicDashboard - if rf, ok := ret.Get(0).(func(context.Context, *SavePublicDashboardConfigDTO) *models.PublicDashboard); ok { - r0 = rf(ctx, dto) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.PublicDashboard) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *SavePublicDashboardConfigDTO) error); ok { - r1 = rf(ctx, dto) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // SearchDashboards provides a mock function with given fields: ctx, query func (_m *FakeDashboardService) SearchDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) error { ret := _m.Called(ctx, query) diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index 2fedc4abf01..f2f3db9c2ac 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -15,13 +15,6 @@ type SaveDashboardDTO struct { Dashboard *models.Dashboard } -type SavePublicDashboardConfigDTO struct { - DashboardUid string - OrgId int64 - UserId int64 - PublicDashboard *models.PublicDashboard -} - type DashboardSearchProjection struct { ID int64 `xorm:"id"` UID string `xorm:"uid"` diff --git a/pkg/services/dashboards/store_mock.go b/pkg/services/dashboards/store_mock.go index 8ecefb4e05c..ddf976aa679 100644 --- a/pkg/services/dashboards/store_mock.go +++ b/pkg/services/dashboards/store_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.12.1. DO NOT EDIT. +// Code generated by mockery v2.12.2. DO NOT EDIT. package dashboards @@ -67,27 +67,6 @@ func (_m *FakeDashboardStore) FindDashboards(ctx context.Context, query *models. return r0, r1 } -// GenerateNewPublicDashboardUid provides a mock function with given fields: ctx -func (_m *FakeDashboardStore) GenerateNewPublicDashboardUid(ctx context.Context) (string, error) { - ret := _m.Called(ctx) - - var r0 string - if rf, ok := ret.Get(0).(func(context.Context) string); ok { - r0 = rf(ctx) - } else { - r0 = ret.Get(0).(string) - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // GetDashboard provides a mock function with given fields: ctx, query func (_m *FakeDashboardStore) GetDashboard(ctx context.Context, query *models.GetDashboardQuery) (*models.Dashboard, error) { ret := _m.Called(ctx, query) @@ -319,61 +298,6 @@ func (_m *FakeDashboardStore) GetProvisionedDataByDashboardUID(orgID int64, dash return r0, r1 } -// GetPublicDashboard provides a mock function with given fields: ctx, accessToken -func (_m *FakeDashboardStore) GetPublicDashboard(ctx context.Context, accessToken string) (*models.PublicDashboard, *models.Dashboard, error) { - ret := _m.Called(ctx, accessToken) - - var r0 *models.PublicDashboard - if rf, ok := ret.Get(0).(func(context.Context, string) *models.PublicDashboard); ok { - r0 = rf(ctx, accessToken) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.PublicDashboard) - } - } - - var r1 *models.Dashboard - if rf, ok := ret.Get(1).(func(context.Context, string) *models.Dashboard); ok { - r1 = rf(ctx, accessToken) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*models.Dashboard) - } - } - - var r2 error - if rf, ok := ret.Get(2).(func(context.Context, string) error); ok { - r2 = rf(ctx, accessToken) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// GetPublicDashboardConfig provides a mock function with given fields: ctx, orgId, dashboardUid -func (_m *FakeDashboardStore) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) { - ret := _m.Called(ctx, orgId, dashboardUid) - - var r0 *models.PublicDashboard - if rf, ok := ret.Get(0).(func(context.Context, int64, string) *models.PublicDashboard); ok { - r0 = rf(ctx, orgId, dashboardUid) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.PublicDashboard) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok { - r1 = rf(ctx, orgId, dashboardUid) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // HasAdminPermissionInDashboardsOrFolders provides a mock function with given fields: ctx, query func (_m *FakeDashboardStore) HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *models.HasAdminPermissionInDashboardsOrFoldersQuery) error { ret := _m.Called(ctx, query) @@ -462,29 +386,6 @@ func (_m *FakeDashboardStore) SaveProvisionedDashboard(cmd models.SaveDashboardC return r0, r1 } -// SavePublicDashboardConfig provides a mock function with given fields: ctx, cmd -func (_m *FakeDashboardStore) SavePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboard, error) { - ret := _m.Called(ctx, cmd) - - var r0 *models.PublicDashboard - if rf, ok := ret.Get(0).(func(context.Context, models.SavePublicDashboardConfigCommand) *models.PublicDashboard); ok { - r0 = rf(ctx, cmd) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.PublicDashboard) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, models.SavePublicDashboardConfigCommand) error); ok { - r1 = rf(ctx, cmd) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // UnprovisionDashboard provides a mock function with given fields: ctx, id func (_m *FakeDashboardStore) UnprovisionDashboard(ctx context.Context, id int64) error { ret := _m.Called(ctx, id) @@ -513,20 +414,6 @@ func (_m *FakeDashboardStore) UpdateDashboardACL(ctx context.Context, uid int64, return r0 } -// UpdatePublicDashboardConfig provides a mock function with given fields: ctx, cmd -func (_m *FakeDashboardStore) UpdatePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) error { - ret := _m.Called(ctx, cmd) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, models.SavePublicDashboardConfigCommand) error); ok { - r0 = rf(ctx, cmd) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // ValidateDashboardBeforeSave provides a mock function with given fields: dashboard, overwrite func (_m *FakeDashboardStore) ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error) { ret := _m.Called(dashboard, overwrite) diff --git a/pkg/services/publicdashboards/api/api.go b/pkg/services/publicdashboards/api/api.go new file mode 100644 index 00000000000..65d87fc6568 --- /dev/null +++ b/pkg/services/publicdashboards/api/api.go @@ -0,0 +1,234 @@ +package api + +import ( + "errors" + "net/http" + "strconv" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins/backendplugin" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/publicdashboards" + . "github.com/grafana/grafana/pkg/services/publicdashboards/models" + "github.com/grafana/grafana/pkg/services/query" + "github.com/grafana/grafana/pkg/util" + "github.com/grafana/grafana/pkg/web" +) + +type Api struct { + PublicDashboardService publicdashboards.Service + RouteRegister routing.RouteRegister + AccessControl accesscontrol.AccessControl + QueryDataService *query.Service + Features *featuremgmt.FeatureManager +} + +func ProvideApi( + pd publicdashboards.Service, + rr routing.RouteRegister, + ac accesscontrol.AccessControl, + qds *query.Service, + features *featuremgmt.FeatureManager, +) *Api { + api := &Api{ + PublicDashboardService: pd, + RouteRegister: rr, + AccessControl: ac, + QueryDataService: qds, + Features: features, + } + + // attach api if PublicDashboards feature flag is enabled + if features.IsEnabled(featuremgmt.FlagPublicDashboards) { + api.RegisterAPIEndpoints() + } + + return api +} + +func (api *Api) RegisterAPIEndpoints() { + auth := accesscontrol.Middleware(api.AccessControl) + reqSignedIn := middleware.ReqSignedIn + + // Anonymous access to public dashboard route is configured in pkg/api/api.go + // because it is deeply dependent on the HTTPServer.Index() method and would result in a + // circular dependency + + api.RouteRegister.Get("/api/public/dashboards/:accessToken", routing.Wrap(api.GetPublicDashboard)) + api.RouteRegister.Post("/api/public/dashboards/:accessToken/panels/:panelId/query", routing.Wrap(api.QueryPublicDashboard)) + + // Create/Update Public Dashboard + api.RouteRegister.Get("/api/dashboards/uid/:uid/public-config", auth(reqSignedIn, accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(api.GetPublicDashboardConfig)) + api.RouteRegister.Post("/api/dashboards/uid/:uid/public-config", auth(reqSignedIn, accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(api.SavePublicDashboardConfig)) +} + +// gets public dashboard +func (api *Api) GetPublicDashboard(c *models.ReqContext) response.Response { + accessToken := web.Params(c.Req)[":accessToken"] + + dash, err := api.PublicDashboardService.GetPublicDashboard(c.Req.Context(), accessToken) + if err != nil { + return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard", err) + } + + meta := dtos.DashboardMeta{ + Slug: dash.Slug, + Type: models.DashTypeDB, + CanStar: false, + CanSave: false, + CanEdit: false, + CanAdmin: false, + CanDelete: false, + Created: dash.Created, + Updated: dash.Updated, + Version: dash.Version, + IsFolder: false, + FolderId: dash.FolderId, + PublicDashboardAccessToken: accessToken, + } + + dto := dtos.DashboardFullWithMeta{Meta: meta, Dashboard: dash.Data} + + return response.JSON(http.StatusOK, dto) +} + +// gets public dashboard configuration for dashboard +func (api *Api) GetPublicDashboardConfig(c *models.ReqContext) response.Response { + pdc, err := api.PublicDashboardService.GetPublicDashboardConfig(c.Req.Context(), c.OrgId, web.Params(c.Req)[":uid"]) + if err != nil { + return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard config", err) + } + return response.JSON(http.StatusOK, pdc) +} + +// sets public dashboard configuration for dashboard +func (api *Api) SavePublicDashboardConfig(c *models.ReqContext) response.Response { + pubdash := &PublicDashboard{} + if err := web.Bind(c.Req, pubdash); err != nil { + return response.Error(http.StatusBadRequest, "bad request data", err) + } + + // Always set the org id to the current auth session orgId + pubdash.OrgId = c.OrgId + + dto := SavePublicDashboardConfigDTO{ + OrgId: c.OrgId, + DashboardUid: web.Params(c.Req)[":uid"], + UserId: c.UserId, + PublicDashboard: pubdash, + } + + pubdash, err := api.PublicDashboardService.SavePublicDashboardConfig(c.Req.Context(), &dto) + if err != nil { + return handleDashboardErr(http.StatusInternalServerError, "Failed to save public dashboard configuration", err) + } + + return response.JSON(http.StatusOK, pubdash) +} + +// QueryPublicDashboard returns all results for a given panel on a public dashboard +// POST /api/public/dashboard/:accessToken/panels/:panelId/query +func (api *Api) QueryPublicDashboard(c *models.ReqContext) response.Response { + panelId, err := strconv.ParseInt(web.Params(c.Req)[":panelId"], 10, 64) + if err != nil { + return response.Error(http.StatusBadRequest, "invalid panel ID", err) + } + + dashboard, err := api.PublicDashboardService.GetPublicDashboard(c.Req.Context(), web.Params(c.Req)[":accessToken"]) + if err != nil { + return response.Error(http.StatusInternalServerError, "could not fetch dashboard", err) + } + + publicDashboard, err := api.PublicDashboardService.GetPublicDashboardConfig(c.Req.Context(), dashboard.OrgId, dashboard.Uid) + if err != nil { + return response.Error(http.StatusInternalServerError, "could not fetch public dashboard", err) + } + + reqDTO, err := api.PublicDashboardService.BuildPublicDashboardMetricRequest( + c.Req.Context(), + dashboard, + publicDashboard, + panelId, + ) + if err != nil { + return handleDashboardErr(http.StatusInternalServerError, "Failed to get queries for public dashboard", err) + } + + anonymousUser, err := api.PublicDashboardService.BuildAnonymousUser(c.Req.Context(), dashboard) + + if err != nil { + return response.Error(http.StatusInternalServerError, "could not create anonymous user", err) + } + + resp, err := api.QueryDataService.QueryDataMultipleSources(c.Req.Context(), anonymousUser, c.SkipCache, reqDTO, true) + + if err != nil { + return handleQueryMetricsError(err) + } + return toJsonStreamingResponse(api.Features, resp) +} + +// util to help us unpack dashboard and publicdashboard errors or use default http code and message +// we should look to do some future refactoring of these errors as publicdashboard err is the same as a dashboarderr, just defined in a +// different package. +func handleDashboardErr(defaultCode int, defaultMsg string, err error) response.Response { + var publicDashboardErr PublicDashboardErr + + // handle public dashboard er + if ok := errors.As(err, &publicDashboardErr); ok { + return response.Error(publicDashboardErr.StatusCode, publicDashboardErr.Error(), publicDashboardErr) + } + + // handle dashboard errors as well + var dashboardErr dashboards.DashboardErr + if ok := errors.As(err, &dashboardErr); ok { + return response.Error(dashboardErr.StatusCode, dashboardErr.Error(), dashboardErr) + } + + return response.Error(defaultCode, defaultMsg, err) +} + +// Copied from pkg/api/metrics.go +func handleQueryMetricsError(err error) *response.NormalResponse { + if errors.Is(err, datasources.ErrDataSourceAccessDenied) { + return response.Error(http.StatusForbidden, "Access denied to data source", err) + } + if errors.Is(err, datasources.ErrDataSourceNotFound) { + return response.Error(http.StatusNotFound, "Data source not found", err) + } + var badQuery *query.ErrBadQuery + if errors.As(err, &badQuery) { + return response.Error(http.StatusBadRequest, util.Capitalize(badQuery.Message), err) + } + + if errors.Is(err, backendplugin.ErrPluginNotRegistered) { + return response.Error(http.StatusNotFound, "Plugin not found", err) + } + + return response.Error(http.StatusInternalServerError, "Query data error", err) +} + +// Copied from pkg/api/metrics.go +func toJsonStreamingResponse(features *featuremgmt.FeatureManager, qdr *backend.QueryDataResponse) response.Response { + statusWhenError := http.StatusBadRequest + if features.IsEnabled(featuremgmt.FlagDatasourceQueryMultiStatus) { + statusWhenError = http.StatusMultiStatus + } + + statusCode := http.StatusOK + for _, res := range qdr.Responses { + if res.Error != nil { + statusCode = statusWhenError + } + } + + return response.JSONStreaming(statusCode, qdr) +} diff --git a/pkg/services/publicdashboards/api/api_test.go b/pkg/services/publicdashboards/api/api_test.go new file mode 100644 index 00000000000..4eb0ea1ad4e --- /dev/null +++ b/pkg/services/publicdashboards/api/api_test.go @@ -0,0 +1,589 @@ +package api + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/localcache" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" + dashboardStore "github.com/grafana/grafana/pkg/services/dashboards/database" + "github.com/grafana/grafana/pkg/services/datasources" + fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes" + "github.com/grafana/grafana/pkg/services/datasources/service" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/publicdashboards" + publicdashboardsStore "github.com/grafana/grafana/pkg/services/publicdashboards/database" + . "github.com/grafana/grafana/pkg/services/publicdashboards/models" + publicdashboardsService "github.com/grafana/grafana/pkg/services/publicdashboards/service" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/web" +) + +func TestAPIGetPublicDashboard(t *testing.T) { + t.Run("It should 404 if featureflag is not enabled", func(t *testing.T) { + cfg := setting.NewCfg() + qs := buildQueryDataService(t, nil, nil, nil) + service := publicdashboards.NewFakePublicDashboardService(t) + service.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")). + Return(&models.Dashboard{}, nil).Maybe() + + testServer := setupTestServer(t, cfg, qs, featuremgmt.WithFeatures(), service, nil) + + response := callAPI(testServer, http.MethodGet, "/api/public/dashboards", nil, t) + assert.Equal(t, http.StatusNotFound, response.Code) + + response = callAPI(testServer, http.MethodGet, "/api/public/dashboards/asdf", nil, t) + assert.Equal(t, http.StatusNotFound, response.Code) + + // control set. make sure routes are mounted + testServer = setupTestServer(t, cfg, qs, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), service, nil) + response = callAPI(testServer, http.MethodGet, "/api/public/dashboards/asdf", nil, t) + assert.NotEqual(t, http.StatusNotFound, response.Code) + }) + + DashboardUid := "dashboard-abcd1234" + token, err := uuid.NewRandom() + require.NoError(t, err) + accessToken := fmt.Sprintf("%x", token) + + testCases := []struct { + Name string + AccessToken string + ExpectedHttpResponse int + PublicDashboardResult *models.Dashboard + PublicDashboardErr error + }{ + { + Name: "It gets a public dashboard", + AccessToken: accessToken, + ExpectedHttpResponse: http.StatusOK, + PublicDashboardResult: &models.Dashboard{ + Data: simplejson.NewFromAny(map[string]interface{}{ + "Uid": DashboardUid, + }), + }, + PublicDashboardErr: nil, + }, + { + Name: "It should return 404 if no public dashboard", + AccessToken: accessToken, + ExpectedHttpResponse: http.StatusNotFound, + PublicDashboardResult: nil, + PublicDashboardErr: ErrPublicDashboardNotFound, + }, + } + + for _, test := range testCases { + t.Run(test.Name, func(t *testing.T) { + service := publicdashboards.NewFakePublicDashboardService(t) + service.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")). + Return(test.PublicDashboardResult, test.PublicDashboardErr).Maybe() + + testServer := setupTestServer( + t, + setting.NewCfg(), + buildQueryDataService(t, nil, nil, nil), + featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), + service, + nil, + ) + + response := callAPI(testServer, http.MethodGet, + fmt.Sprintf("/api/public/dashboards/%s", test.AccessToken), + nil, + t, + ) + + assert.Equal(t, test.ExpectedHttpResponse, response.Code) + + if test.PublicDashboardErr == nil { + var dashResp dtos.DashboardFullWithMeta + err := json.Unmarshal(response.Body.Bytes(), &dashResp) + require.NoError(t, err) + + assert.Equal(t, DashboardUid, dashResp.Dashboard.Get("Uid").MustString()) + assert.Equal(t, false, dashResp.Meta.CanEdit) + assert.Equal(t, false, dashResp.Meta.CanDelete) + assert.Equal(t, false, dashResp.Meta.CanSave) + } else { + var errResp struct { + Error string `json:"error"` + } + err := json.Unmarshal(response.Body.Bytes(), &errResp) + require.NoError(t, err) + assert.Equal(t, test.PublicDashboardErr.Error(), errResp.Error) + } + }) + } +} + +func TestAPIGetPublicDashboardConfig(t *testing.T) { + pubdash := &PublicDashboard{IsEnabled: true} + + testCases := []struct { + Name string + DashboardUid string + ExpectedHttpResponse int + PublicDashboardResult *PublicDashboard + PublicDashboardErr error + }{ + { + Name: "retrieves public dashboard config when dashboard is found", + DashboardUid: "1", + ExpectedHttpResponse: http.StatusOK, + PublicDashboardResult: pubdash, + PublicDashboardErr: nil, + }, + { + Name: "returns 404 when dashboard not found", + DashboardUid: "77777", + ExpectedHttpResponse: http.StatusNotFound, + PublicDashboardResult: nil, + PublicDashboardErr: dashboards.ErrDashboardNotFound, + }, + { + Name: "returns 500 when internal server error", + DashboardUid: "1", + ExpectedHttpResponse: http.StatusInternalServerError, + PublicDashboardResult: nil, + PublicDashboardErr: errors.New("database broken"), + }, + } + + for _, test := range testCases { + t.Run(test.Name, func(t *testing.T) { + service := publicdashboards.NewFakePublicDashboardService(t) + service.On("GetPublicDashboardConfig", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")). + Return(test.PublicDashboardResult, test.PublicDashboardErr) + + testServer := setupTestServer( + t, + setting.NewCfg(), + buildQueryDataService(t, nil, nil, nil), + featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), + service, + nil, + ) + + response := callAPI( + testServer, + http.MethodGet, + "/api/dashboards/uid/1/public-config", + nil, + t, + ) + + assert.Equal(t, test.ExpectedHttpResponse, response.Code) + + if response.Code == http.StatusOK { + var pdcResp PublicDashboard + err := json.Unmarshal(response.Body.Bytes(), &pdcResp) + require.NoError(t, err) + assert.Equal(t, test.PublicDashboardResult, &pdcResp) + } + }) + } +} + +func TestApiSavePublicDashboardConfig(t *testing.T) { + testCases := []struct { + Name string + DashboardUid string + publicDashboardConfig *PublicDashboard + ExpectedHttpResponse int + SaveDashboardErr error + }{ + { + Name: "returns 200 when update persists", + DashboardUid: "1", + publicDashboardConfig: &PublicDashboard{IsEnabled: true}, + ExpectedHttpResponse: http.StatusOK, + SaveDashboardErr: nil, + }, + { + Name: "returns 500 when not persisted", + ExpectedHttpResponse: http.StatusInternalServerError, + publicDashboardConfig: &PublicDashboard{}, + SaveDashboardErr: errors.New("backend failed to save"), + }, + { + Name: "returns 404 when dashboard not found", + ExpectedHttpResponse: http.StatusNotFound, + publicDashboardConfig: &PublicDashboard{}, + SaveDashboardErr: dashboards.ErrDashboardNotFound, + }, + } + + for _, test := range testCases { + t.Run(test.Name, func(t *testing.T) { + service := publicdashboards.NewFakePublicDashboardService(t) + service.On("SavePublicDashboardConfig", mock.Anything, mock.AnythingOfType("*models.SavePublicDashboardConfigDTO")). + Return(&PublicDashboard{IsEnabled: true}, test.SaveDashboardErr) + + testServer := setupTestServer( + t, + setting.NewCfg(), + buildQueryDataService(t, nil, nil, nil), + featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), + service, + nil, + ) + + response := callAPI( + testServer, + http.MethodPost, + "/api/dashboards/uid/1/public-config", + strings.NewReader(`{ "isPublic": true }`), + t, + ) + + assert.Equal(t, test.ExpectedHttpResponse, response.Code) + + //check the result if it's a 200 + if response.Code == http.StatusOK { + val, err := json.Marshal(test.publicDashboardConfig) + require.NoError(t, err) + assert.Equal(t, string(val), response.Body.String()) + } + }) + } +} + +// `/public/dashboards/:uid/query`` endpoint test +func TestAPIQueryPublicDashboard(t *testing.T) { + cacheService := &fakeDatasources.FakeCacheService{ + DataSources: []*datasources.DataSource{ + {Uid: "mysqlds"}, + {Uid: "promds"}, + {Uid: "promds2"}, + }, + } + + // used to determine whether fakePluginClient returns an error + queryReturnsError := false + + fakePluginClient := &fakePluginClient{ + QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + if queryReturnsError { + return nil, errors.New("error") + } + + resp := backend.Responses{} + + for _, query := range req.Queries { + resp[query.RefID] = backend.DataResponse{ + Frames: []*data.Frame{ + { + RefID: query.RefID, + Name: "query-" + query.RefID, + }, + }, + } + } + return &backend.QueryDataResponse{Responses: resp}, nil + }, + } + + qds := buildQueryDataService(t, cacheService, fakePluginClient, nil) + + setup := func(enabled bool) (*web.Mux, *publicdashboards.FakePublicDashboardService) { + service := publicdashboards.NewFakePublicDashboardService(t) + + testServer := setupTestServer( + t, + setting.NewCfg(), + qds, + featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards, enabled), + service, + nil, + ) + + return testServer, service + } + + t.Run("Status code is 404 when feature toggle is disabled", func(t *testing.T) { + server, _ := setup(false) + resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t) + require.Equal(t, http.StatusNotFound, resp.Code) + }) + + t.Run("Status code is 400 when the panel ID is invalid", func(t *testing.T) { + server, _ := setup(true) + resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/notanumber/query", strings.NewReader("{}"), t) + require.Equal(t, http.StatusBadRequest, resp.Code) + }) + + t.Run("Returns query data when feature toggle is enabled", func(t *testing.T) { + server, fakeDashboardService := setup(true) + + fakeDashboardService.On("GetPublicDashboard", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil) + fakeDashboardService.On("GetPublicDashboardConfig", mock.Anything, mock.Anything, mock.Anything).Return(&PublicDashboard{}, nil) + fakeDashboardService.On("BuildAnonymousUser", mock.Anything, mock.Anything, mock.Anything).Return(&models.SignedInUser{}, nil) + fakeDashboardService.On("BuildPublicDashboardMetricRequest", mock.Anything, mock.Anything, mock.Anything, int64(2)).Return(dtos.MetricRequest{ + Queries: []*simplejson.Json{ + simplejson.MustJson([]byte(` + { + "datasource": { + "type": "prometheus", + "uid": "promds" + }, + "exemplar": true, + "expr": "query_2_A", + "interval": "", + "legendFormat": "", + "refId": "A" + } + `)), + }, + }, nil) + + resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t) + + require.JSONEq( + t, + `{ + "results": { + "A": { + "frames": [ + { + "data": { + "values": [] + }, + "schema": { + "fields": [], + "refId": "A", + "name": "query-A" + } + } + ] + } + } + }`, + resp.Body.String(), + ) + require.Equal(t, http.StatusOK, resp.Code) + }) + + t.Run("Status code is 500 when the query fails", func(t *testing.T) { + server, fakeDashboardService := setup(true) + + fakeDashboardService.On("GetPublicDashboard", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil) + fakeDashboardService.On("GetPublicDashboardConfig", mock.Anything, mock.Anything, mock.Anything).Return(&PublicDashboard{}, nil) + fakeDashboardService.On("BuildAnonymousUser", mock.Anything, mock.Anything, mock.Anything).Return(&models.SignedInUser{}, nil) + fakeDashboardService.On("BuildPublicDashboardMetricRequest", mock.Anything, mock.Anything, mock.Anything, int64(2)).Return(dtos.MetricRequest{ + Queries: []*simplejson.Json{ + simplejson.MustJson([]byte(` + { + "datasource": { + "type": "prometheus", + "uid": "promds" + }, + "exemplar": true, + "expr": "query_2_A", + "interval": "", + "legendFormat": "", + "refId": "A" + } + `)), + }, + }, nil) + + queryReturnsError = true + resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t) + require.Equal(t, http.StatusInternalServerError, resp.Code) + queryReturnsError = false + }) + + t.Run("Status code is 200 when a panel has queries from multiple datasources", func(t *testing.T) { + server, fakeDashboardService := setup(true) + + fakeDashboardService.On("GetPublicDashboard", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil) + fakeDashboardService.On("GetPublicDashboardConfig", mock.Anything, mock.Anything, mock.Anything).Return(&PublicDashboard{}, nil) + fakeDashboardService.On("BuildAnonymousUser", mock.Anything, mock.Anything, mock.Anything).Return(&models.SignedInUser{}, nil) + fakeDashboardService.On("BuildPublicDashboardMetricRequest", mock.Anything, mock.Anything, mock.Anything, int64(2)).Return(dtos.MetricRequest{ + Queries: []*simplejson.Json{ + simplejson.MustJson([]byte(` +{ + "datasource": { + "type": "prometheus", + "uid": "promds" + }, + "exemplar": true, + "expr": "query_2_A", + "interval": "", + "legendFormat": "", + "refId": "A" + } + `)), + simplejson.MustJson([]byte(` +{ + "datasource": { + "type": "prometheus", + "uid": "promds2" + }, + "exemplar": true, + "expr": "query_2_B", + "interval": "", + "legendFormat": "", + "refId": "B" + } + `)), + }, + }, nil) + + resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t) + require.JSONEq( + t, + `{ + "results": { + "A": { + "frames": [ + { + "data": { + "values": [] + }, + "schema": { + "fields": [], + "refId": "A", + "name": "query-A" + } + } + ] + }, + "B": { + "frames": [ + { + "data": { + "values": [] + }, + "schema": { + "fields": [], + "refId": "B", + "name": "query-B" + } + } + ] + } + } + }`, + resp.Body.String(), + ) + require.Equal(t, http.StatusOK, resp.Code) + }) +} + +func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) { + db := sqlstore.InitTestDB(t) + + cacheService := service.ProvideCacheService(localcache.ProvideService(), db) + qds := buildQueryDataService(t, cacheService, nil, db) + + _ = db.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{ + Uid: "ds1", + OrgId: 1, + Name: "laban", + Type: datasources.DS_MYSQL, + Access: datasources.DS_ACCESS_DIRECT, + Url: "http://test", + Database: "site", + ReadOnly: true, + }) + + // Create Dashboard + saveDashboardCmd := models.SaveDashboardCommand{ + OrgId: 1, + FolderId: 1, + IsFolder: false, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": "test", + "panels": []map[string]interface{}{ + { + "id": 1, + "targets": []map[string]interface{}{ + { + "datasource": map[string]string{ + "type": "mysql", + "uid": "ds1", + }, + "refId": "A", + }, + }, + }, + }, + }), + } + + // create dashboard + dashboardStore := dashboardStore.ProvideDashboardStore(db) + dashboard, err := dashboardStore.SaveDashboard(saveDashboardCmd) + require.NoError(t, err) + + // Create public dashboard + savePubDashboardCmd := &SavePublicDashboardConfigDTO{ + DashboardUid: dashboard.Uid, + OrgId: dashboard.OrgId, + PublicDashboard: &PublicDashboard{ + IsEnabled: true, + }, + } + + // create public dashboard + store := publicdashboardsStore.ProvideStore(db) + service := publicdashboardsService.ProvideService(setting.NewCfg(), store) + pubdash, err := service.SavePublicDashboardConfig(context.Background(), savePubDashboardCmd) + require.NoError(t, err) + + // setup test server + server := setupTestServer(t, + setting.NewCfg(), + qds, + featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), + service, + db, + ) + + resp := callAPI(server, http.MethodPost, + fmt.Sprintf("/api/public/dashboards/%s/panels/1/query", pubdash.AccessToken), + strings.NewReader(`{}`), + t, + ) + require.Equal(t, http.StatusOK, resp.Code) + require.NoError(t, err) + require.JSONEq( + t, + `{ + "results": { + "A": { + "frames": [ + { + "data": { + "values": [] + }, + "schema": { + "fields": [] + } + } + ] + } + } + }`, + resp.Body.String(), + ) +} diff --git a/pkg/services/publicdashboards/api/common_test.go b/pkg/services/publicdashboards/api/common_test.go new file mode 100644 index 00000000000..d500c8d6e90 --- /dev/null +++ b/pkg/services/publicdashboards/api/common_test.go @@ -0,0 +1,174 @@ +package api + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "testing" + + "golang.org/x/oauth2" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/infra/localcache" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/services/accesscontrol/database" + "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" + "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/publicdashboards" + "github.com/grafana/grafana/pkg/services/sqlstore" + + fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes" + datasourceService "github.com/grafana/grafana/pkg/services/datasources/service" + "github.com/grafana/grafana/pkg/services/query" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/web" + "github.com/stretchr/testify/require" +) + +type Server struct { + Mux *web.Mux + RouteRegister routing.RouteRegister + TestServer *httptest.Server +} + +func setupTestServer( + t *testing.T, + cfg *setting.Cfg, + qs *query.Service, + features *featuremgmt.FeatureManager, + service publicdashboards.Service, + db *sqlstore.SQLStore, +) *web.Mux { + // build router to register routes + rr := routing.NewRouteRegister() + + // build access control - FIXME we should be able to mock this, but to get + // tests going, we're going to instantiate full accesscontrol + //ac := accesscontrolmock.New() + //ac.WithDisabled() + + // create a sqlstore for access control. + if db == nil { + db = sqlstore.InitTestDB(t) + } + + var err error + ac, err := ossaccesscontrol.ProvideService(features, cfg, database.ProvideService(db), rr) + require.NoError(t, err) + + // build mux + m := web.New() + + // set initial context + m.Use(func(c *web.Context) { + ctx := &models.ReqContext{ + Context: c, + IsSignedIn: true, // FIXME need to be able to change this for tests + SkipCache: true, // hardcoded to make sure query service doesnt hit the cache + Logger: log.New("publicdashboards-test"), + + // Set signed in user. We might not actually need to do this. + SignedInUser: &models.SignedInUser{UserId: 1, OrgId: 1, OrgRole: models.ROLE_ADMIN, Login: "testUser"}, + } + c.Req = c.Req.WithContext(ctxkey.Set(c.Req.Context(), ctx)) + }) + + // build api, this will mount the routes at the same time if + // featuremgmt.FlagPublicDashboard is enabled + ProvideApi(service, rr, ac, qs, features) + + // connect routes to mux + rr.Register(m.Router) + + return m +} + +func callAPI(server *web.Mux, method, path string, body io.Reader, t *testing.T) *httptest.ResponseRecorder { + req, err := http.NewRequest(method, path, body) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + recorder := httptest.NewRecorder() + server.ServeHTTP(recorder, req) + return recorder +} + +// helper to query.Service +// allows us to stub the cache and plugin clients +func buildQueryDataService(t *testing.T, cs datasources.CacheService, fpc *fakePluginClient, store *sqlstore.SQLStore) *query.Service { + // build database if we need one + if store == nil { + store = sqlstore.InitTestDB(t) + } + + // default cache service + if cs == nil { + cs = datasourceService.ProvideCacheService(localcache.ProvideService(), store) + } + + // default fakePluginClient + if fpc == nil { + fpc = &fakePluginClient{ + QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + resp := backend.Responses{ + "A": backend.DataResponse{ + Frames: []*data.Frame{{}}, + }, + } + return &backend.QueryDataResponse{Responses: resp}, nil + }, + } + } + + return query.ProvideService( + nil, + cs, + nil, + &fakePluginRequestValidator{}, + &fakeDatasources.FakeDataSourceService{}, + fpc, + &fakeOAuthTokenService{}, + ) +} + +//copied from pkg/api/metrics_test.go +type fakePluginRequestValidator struct { + err error +} + +func (rv *fakePluginRequestValidator) Validate(dsURL string, req *http.Request) error { + return rv.err +} + +type fakeOAuthTokenService struct { + passThruEnabled bool + token *oauth2.Token +} + +func (ts *fakeOAuthTokenService) GetCurrentOAuthToken(context.Context, *models.SignedInUser) *oauth2.Token { + return ts.token +} + +func (ts *fakeOAuthTokenService) IsOAuthPassThruEnabled(*datasources.DataSource) bool { + return ts.passThruEnabled +} + +// copied from pkg/api/plugins_test.go +type fakePluginClient struct { + plugins.Client + backend.QueryDataHandlerFunc +} + +func (c *fakePluginClient) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + if c.QueryDataHandlerFunc != nil { + return c.QueryDataHandlerFunc.QueryData(ctx, req) + } + + return backend.NewQueryDataResponse(), nil +} diff --git a/pkg/middleware/dashboard_public.go b/pkg/services/publicdashboards/api/middleware.go similarity index 65% rename from pkg/middleware/dashboard_public.go rename to pkg/services/publicdashboards/api/middleware.go index 3c89e8ac566..f7ba59d587a 100644 --- a/pkg/middleware/dashboard_public.go +++ b/pkg/services/publicdashboards/api/middleware.go @@ -1,8 +1,6 @@ -package middleware +package api -import ( - "github.com/grafana/grafana/pkg/models" -) +import "github.com/grafana/grafana/pkg/models" func SetPublicDashboardFlag() func(c *models.ReqContext) { return func(c *models.ReqContext) { diff --git a/pkg/services/dashboards/database/database_dashboard_public.go b/pkg/services/publicdashboards/database/database.go similarity index 53% rename from pkg/services/dashboards/database/database_dashboard_public.go rename to pkg/services/publicdashboards/database/database.go index e1ae039b1ab..d89687c1526 100644 --- a/pkg/services/dashboards/database/database_dashboard_public.go +++ b/pkg/services/publicdashboards/database/database.go @@ -3,27 +3,52 @@ package database import ( "context" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/publicdashboards" + . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/util" ) -// retrieves public dashboard configuration -func (d *DashboardStore) GetPublicDashboard(ctx context.Context, accessToken string) (*models.PublicDashboard, *models.Dashboard, error) { +// Define the storage implementation. We're generating the mock implementation +// automatically +type PublicDashboardStoreImpl struct { + sqlStore *sqlstore.SQLStore + log log.Logger + dialect migrator.Dialect +} + +// Gives us a compile time error if our database does not adhere to contract of +// the interface +var _ publicdashboards.Store = (*PublicDashboardStoreImpl)(nil) + +// Factory used by wire to dependency injection +func ProvideStore(sqlStore *sqlstore.SQLStore) *PublicDashboardStoreImpl { + return &PublicDashboardStoreImpl{ + sqlStore: sqlStore, + log: log.New("publicdashboards.store"), + dialect: sqlStore.Dialect, + } +} + +// Retrieves public dashboard configuration +func (d *PublicDashboardStoreImpl) GetPublicDashboard(ctx context.Context, accessToken string) (*PublicDashboard, *models.Dashboard, error) { if accessToken == "" { - return nil, nil, dashboards.ErrPublicDashboardIdentifierNotSet + return nil, nil, ErrPublicDashboardIdentifierNotSet } // get public dashboard - pdRes := &models.PublicDashboard{AccessToken: accessToken} + pdRes := &PublicDashboard{AccessToken: accessToken} err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { has, err := sess.Get(pdRes) if err != nil { return err } if !has { - return dashboards.ErrPublicDashboardNotFound + return ErrPublicDashboardNotFound } return nil }) @@ -40,7 +65,7 @@ func (d *DashboardStore) GetPublicDashboard(ctx context.Context, accessToken str return err } if !has { - return dashboards.ErrPublicDashboardNotFound + return ErrPublicDashboardNotFound } return nil }) @@ -52,15 +77,15 @@ func (d *DashboardStore) GetPublicDashboard(ctx context.Context, accessToken str return pdRes, dashRes, err } -// generates a new unique uid to retrieve a public dashboard -func (d *DashboardStore) GenerateNewPublicDashboardUid(ctx context.Context) (string, error) { +// Generates a new unique uid to retrieve a public dashboard +func (d *PublicDashboardStoreImpl) GenerateNewPublicDashboardUid(ctx context.Context) (string, error) { var uid string err := d.sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { for i := 0; i < 3; i++ { uid = util.GenerateShortUID() - exists, err := sess.Get(&models.PublicDashboard{Uid: uid}) + exists, err := sess.Get(&PublicDashboard{Uid: uid}) if err != nil { return err } @@ -70,7 +95,7 @@ func (d *DashboardStore) GenerateNewPublicDashboardUid(ctx context.Context) (str } } - return dashboards.ErrPublicDashboardFailedGenerateUniqueUid + return ErrPublicDashboardFailedGenerateUniqueUid }) if err != nil { @@ -80,13 +105,13 @@ func (d *DashboardStore) GenerateNewPublicDashboardUid(ctx context.Context) (str return uid, nil } -// retrieves public dashboard configuration -func (d *DashboardStore) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) { +// Retrieves public dashboard configuration +func (d *PublicDashboardStoreImpl) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error) { if dashboardUid == "" { return nil, dashboards.ErrDashboardIdentifierNotSet } - pdRes := &models.PublicDashboard{OrgId: orgId, DashboardUid: dashboardUid} + pdRes := &PublicDashboard{OrgId: orgId, DashboardUid: dashboardUid} err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { // publicDashboard _, err := sess.Get(pdRes) @@ -104,8 +129,8 @@ func (d *DashboardStore) GetPublicDashboardConfig(ctx context.Context, orgId int return pdRes, err } -// persists public dashboard configuration -func (d *DashboardStore) SavePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboard, error) { +// Persists public dashboard configuration +func (d *PublicDashboardStoreImpl) SavePublicDashboardConfig(ctx context.Context, cmd SavePublicDashboardConfigCommand) (*PublicDashboard, error) { err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { _, err := sess.UseBool("is_enabled").Insert(&cmd.PublicDashboard) if err != nil { @@ -123,7 +148,7 @@ func (d *DashboardStore) SavePublicDashboardConfig(ctx context.Context, cmd mode } // updates existing public dashboard configuration -func (d *DashboardStore) UpdatePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) error { +func (d *PublicDashboardStoreImpl) UpdatePublicDashboardConfig(ctx context.Context, cmd SavePublicDashboardConfigCommand) error { err := d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { timeSettingsJSON, err := cmd.PublicDashboard.TimeSettings.MarshalJSON() if err != nil { diff --git a/pkg/services/dashboards/database/database_dashboard_public_test.go b/pkg/services/publicdashboards/database/database_test.go similarity index 62% rename from pkg/services/dashboards/database/database_dashboard_public_test.go rename to pkg/services/publicdashboards/database/database_test.go index 6462b9bf50a..ec335f13714 100644 --- a/pkg/services/dashboards/database/database_dashboard_public_test.go +++ b/pkg/services/publicdashboards/database/database_test.go @@ -10,8 +10,10 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/dashboards" + dashboards "github.com/grafana/grafana/pkg/services/dashboards" + dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/featuremgmt" + . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/util" ) @@ -25,19 +27,21 @@ var DefaultTime = time.Now().UTC().Round(time.Second) // GetPublicDashboard func TestIntegrationGetPublicDashboard(t *testing.T) { var sqlStore *sqlstore.SQLStore - var dashboardStore *DashboardStore + var dashboardStore *dashboardsDB.DashboardStore + var publicdashboardStore *PublicDashboardStoreImpl var savedDashboard *models.Dashboard setup := func() { sqlStore = sqlstore.InitTestDB(t) - dashboardStore = ProvideDashboardStore(sqlStore) + dashboardStore = dashboardsDB.ProvideDashboardStore(sqlStore) + publicdashboardStore = ProvideStore(sqlStore) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) } t.Run("returns PublicDashboard and Dashboard", func(t *testing.T) { setup() - pubdash, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{ - PublicDashboard: models.PublicDashboard{ + pubdash, err := publicdashboardStore.SavePublicDashboardConfig(context.Background(), SavePublicDashboardConfigCommand{ + PublicDashboard: PublicDashboard{ IsEnabled: true, Uid: "abc1234", DashboardUid: savedDashboard.Uid, @@ -50,7 +54,7 @@ func TestIntegrationGetPublicDashboard(t *testing.T) { }) require.NoError(t, err) - pd, d, err := dashboardStore.GetPublicDashboard(context.Background(), "NOTAREALUUID") + pd, d, err := publicdashboardStore.GetPublicDashboard(context.Background(), "NOTAREALUUID") require.NoError(t, err) assert.Equal(t, pd, pubdash) @@ -59,22 +63,22 @@ func TestIntegrationGetPublicDashboard(t *testing.T) { t.Run("returns ErrPublicDashboardNotFound with empty uid", func(t *testing.T) { setup() - _, _, err := dashboardStore.GetPublicDashboard(context.Background(), "") - require.Error(t, dashboards.ErrPublicDashboardIdentifierNotSet, err) + _, _, err := publicdashboardStore.GetPublicDashboard(context.Background(), "") + require.Error(t, ErrPublicDashboardIdentifierNotSet, err) }) t.Run("returns ErrPublicDashboardNotFound when PublicDashboard not found", func(t *testing.T) { setup() - _, _, err := dashboardStore.GetPublicDashboard(context.Background(), "zzzzzz") - require.Error(t, dashboards.ErrPublicDashboardNotFound, err) + _, _, err := publicdashboardStore.GetPublicDashboard(context.Background(), "zzzzzz") + require.Error(t, ErrPublicDashboardNotFound, err) }) t.Run("returns ErrDashboardNotFound when Dashboard not found", func(t *testing.T) { setup() - _, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{ + _, err := publicdashboardStore.SavePublicDashboardConfig(context.Background(), SavePublicDashboardConfigCommand{ DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId, - PublicDashboard: models.PublicDashboard{ + PublicDashboard: PublicDashboard{ IsEnabled: true, Uid: "abc1234", DashboardUid: "nevergonnafindme", @@ -84,7 +88,7 @@ func TestIntegrationGetPublicDashboard(t *testing.T) { }, }) require.NoError(t, err) - _, _, err = dashboardStore.GetPublicDashboard(context.Background(), "abc1234") + _, _, err = publicdashboardStore.GetPublicDashboard(context.Background(), "abc1234") require.Error(t, dashboards.ErrDashboardNotFound, err) }) } @@ -92,35 +96,37 @@ func TestIntegrationGetPublicDashboard(t *testing.T) { // GetPublicDashboardConfig func TestIntegrationGetPublicDashboardConfig(t *testing.T) { var sqlStore *sqlstore.SQLStore - var dashboardStore *DashboardStore + var dashboardStore *dashboardsDB.DashboardStore + var publicdashboardStore *PublicDashboardStoreImpl var savedDashboard *models.Dashboard setup := func() { sqlStore = sqlstore.InitTestDB(t) - dashboardStore = ProvideDashboardStore(sqlStore) + dashboardStore = dashboardsDB.ProvideDashboardStore(sqlStore) + publicdashboardStore = ProvideStore(sqlStore) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) } t.Run("returns isPublic and set dashboardUid and orgId", func(t *testing.T) { setup() - pubdash, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid) + pubdash, err := publicdashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid) require.NoError(t, err) - assert.Equal(t, &models.PublicDashboard{IsEnabled: false, DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId}, pubdash) + assert.Equal(t, &PublicDashboard{IsEnabled: false, DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId}, pubdash) }) t.Run("returns dashboard errDashboardIdentifierNotSet", func(t *testing.T) { setup() - _, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, "") + _, err := publicdashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, "") require.Error(t, dashboards.ErrDashboardIdentifierNotSet, err) }) t.Run("returns isPublic along with public dashboard when exists", func(t *testing.T) { setup() // insert test public dashboard - resp, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{ + resp, err := publicdashboardStore.SavePublicDashboardConfig(context.Background(), SavePublicDashboardConfigCommand{ DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId, - PublicDashboard: models.PublicDashboard{ + PublicDashboard: PublicDashboard{ IsEnabled: true, Uid: "pubdash-uid", DashboardUid: savedDashboard.Uid, @@ -132,7 +138,7 @@ func TestIntegrationGetPublicDashboardConfig(t *testing.T) { }) require.NoError(t, err) - pubdash, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid) + pubdash, err := publicdashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid) require.NoError(t, err) assert.True(t, assert.ObjectsAreEqualValues(resp, pubdash)) @@ -143,23 +149,25 @@ func TestIntegrationGetPublicDashboardConfig(t *testing.T) { // SavePublicDashboardConfig func TestIntegrationSavePublicDashboardConfig(t *testing.T) { var sqlStore *sqlstore.SQLStore - var dashboardStore *DashboardStore + var dashboardStore *dashboardsDB.DashboardStore + var publicdashboardStore *PublicDashboardStoreImpl var savedDashboard *models.Dashboard var savedDashboard2 *models.Dashboard setup := func() { sqlStore = sqlstore.InitTestDB(t, sqlstore.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}}) - dashboardStore = ProvideDashboardStore(sqlStore) + dashboardStore = dashboardsDB.ProvideDashboardStore(sqlStore) + publicdashboardStore = ProvideStore(sqlStore) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) savedDashboard2 = insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, true) } t.Run("saves new public dashboard", func(t *testing.T) { setup() - resp, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{ + resp, err := publicdashboardStore.SavePublicDashboardConfig(context.Background(), SavePublicDashboardConfigCommand{ DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId, - PublicDashboard: models.PublicDashboard{ + PublicDashboard: PublicDashboard{ IsEnabled: true, Uid: "pubdash-uid", DashboardUid: savedDashboard.Uid, @@ -172,7 +180,7 @@ func TestIntegrationSavePublicDashboardConfig(t *testing.T) { }) require.NoError(t, err) - pubdash, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid) + pubdash, err := publicdashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid) require.NoError(t, err) //verify saved response and queried response are the same @@ -182,7 +190,7 @@ func TestIntegrationSavePublicDashboardConfig(t *testing.T) { assert.True(t, util.IsValidShortUID(pubdash.Uid)) // verify we didn't update all dashboards - pubdash2, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard2.OrgId, savedDashboard2.Uid) + pubdash2, err := publicdashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard2.OrgId, savedDashboard2.Uid) require.NoError(t, err) assert.False(t, pubdash2.IsEnabled) }) @@ -190,13 +198,15 @@ func TestIntegrationSavePublicDashboardConfig(t *testing.T) { func TestIntegrationUpdatePublicDashboard(t *testing.T) { var sqlStore *sqlstore.SQLStore - var dashboardStore *DashboardStore + var dashboardStore *dashboardsDB.DashboardStore + var publicdashboardStore *PublicDashboardStoreImpl var savedDashboard *models.Dashboard var anotherSavedDashboard *models.Dashboard setup := func() { sqlStore = sqlstore.InitTestDB(t, sqlstore.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}}) - dashboardStore = ProvideDashboardStore(sqlStore) + dashboardStore = dashboardsDB.ProvideDashboardStore(sqlStore) + publicdashboardStore = ProvideStore(sqlStore) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) anotherSavedDashboard = insertTestDashboard(t, dashboardStore, "test another Dashie", 1, 0, true) } @@ -204,28 +214,11 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) { t.Run("updates an existing dashboard", func(t *testing.T) { setup() - // inserting two different public dashboards to test update works and only affect the desired pd by uid - anotherPdUid := "anotherUid" - _, err := dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{ - DashboardUid: anotherSavedDashboard.Uid, - OrgId: anotherSavedDashboard.OrgId, - PublicDashboard: models.PublicDashboard{ - Uid: anotherPdUid, - DashboardUid: anotherSavedDashboard.Uid, - OrgId: anotherSavedDashboard.OrgId, - IsEnabled: true, - CreatedAt: DefaultTime, - CreatedBy: 7, - AccessToken: "fakeaccesstoken", - }, - }) - require.NoError(t, err) - pdUid := "asdf1234" - _, err = dashboardStore.SavePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{ + _, err := publicdashboardStore.SavePublicDashboardConfig(context.Background(), SavePublicDashboardConfigCommand{ DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId, - PublicDashboard: models.PublicDashboard{ + PublicDashboard: PublicDashboard{ Uid: pdUid, DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId, @@ -237,7 +230,24 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) { }) require.NoError(t, err) - updatedPublicDashboard := models.PublicDashboard{ + // inserting two different public dashboards to test update works and only affect the desired pd by uid + anotherPdUid := "anotherUid" + _, err = publicdashboardStore.SavePublicDashboardConfig(context.Background(), SavePublicDashboardConfigCommand{ + DashboardUid: anotherSavedDashboard.Uid, + OrgId: anotherSavedDashboard.OrgId, + PublicDashboard: PublicDashboard{ + Uid: anotherPdUid, + DashboardUid: anotherSavedDashboard.Uid, + OrgId: anotherSavedDashboard.OrgId, + IsEnabled: true, + CreatedAt: DefaultTime, + CreatedBy: 7, + AccessToken: "fakeaccesstoken", + }, + }) + require.NoError(t, err) + + updatedPublicDashboard := PublicDashboard{ Uid: pdUid, DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId, @@ -247,7 +257,7 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) { UpdatedBy: 8, } // update initial record - err = dashboardStore.UpdatePublicDashboardConfig(context.Background(), models.SavePublicDashboardConfigCommand{ + err = publicdashboardStore.UpdatePublicDashboardConfig(context.Background(), SavePublicDashboardConfigCommand{ DashboardUid: savedDashboard.Uid, OrgId: savedDashboard.OrgId, PublicDashboard: updatedPublicDashboard, @@ -255,7 +265,7 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) { require.NoError(t, err) // updated dashboard should have changed - pdRetrieved, err := dashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid) + pdRetrieved, err := publicdashboardStore.GetPublicDashboardConfig(context.Background(), savedDashboard.OrgId, savedDashboard.Uid) require.NoError(t, err) assert.Equal(t, updatedPublicDashboard.UpdatedAt, pdRetrieved.UpdatedAt) @@ -264,10 +274,30 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) { assert.Equal(t, updatedPublicDashboard.IsEnabled, pdRetrieved.IsEnabled) // not updated dashboard shouldn't have changed - pdNotUpdatedRetrieved, err := dashboardStore.GetPublicDashboardConfig(context.Background(), anotherSavedDashboard.OrgId, anotherSavedDashboard.Uid) + pdNotUpdatedRetrieved, err := publicdashboardStore.GetPublicDashboardConfig(context.Background(), anotherSavedDashboard.OrgId, anotherSavedDashboard.Uid) require.NoError(t, err) assert.NotEqual(t, updatedPublicDashboard.UpdatedAt, pdNotUpdatedRetrieved.UpdatedAt) assert.NotEqual(t, updatedPublicDashboard.IsEnabled, pdNotUpdatedRetrieved.IsEnabled) }) } +func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardStore, title string, orgId int64, + folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard { + t.Helper() + cmd := models.SaveDashboardCommand{ + OrgId: orgId, + FolderId: folderId, + IsFolder: isFolder, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": title, + "tags": tags, + }), + } + dash, err := dashboardStore.SaveDashboard(cmd) + require.NoError(t, err) + require.NotNil(t, dash) + dash.Data.Set("id", dash.Id) + dash.Data.Set("uid", dash.Uid) + return dash +} diff --git a/pkg/models/dashboards_public.go b/pkg/services/publicdashboards/models/models.go similarity index 54% rename from pkg/models/dashboards_public.go rename to pkg/services/publicdashboards/models/models.go index a05492c6ea8..31983d53088 100644 --- a/pkg/models/dashboards_public.go +++ b/pkg/services/publicdashboards/models/models.go @@ -4,6 +4,47 @@ import ( "time" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/models" +) + +// PublicDashboardErr represents a dashboard error. +type PublicDashboardErr struct { + StatusCode int + Status string + Reason string +} + +// Error returns the error message. +func (e PublicDashboardErr) Error() string { + if e.Reason != "" { + return e.Reason + } + return "Dashboard Error" +} + +var ( + ErrPublicDashboardFailedGenerateUniqueUid = PublicDashboardErr{ + Reason: "Failed to generate unique public dashboard id", + StatusCode: 500, + } + ErrPublicDashboardFailedGenerateAccesstoken = PublicDashboardErr{ + Reason: "Failed to public dashboard access token", + StatusCode: 500, + } + ErrPublicDashboardNotFound = PublicDashboardErr{ + Reason: "Public dashboard not found", + StatusCode: 404, + Status: "not-found", + } + ErrPublicDashboardPanelNotFound = PublicDashboardErr{ + Reason: "Panel not found in dashboard", + StatusCode: 404, + Status: "not-found", + } + ErrPublicDashboardIdentifierNotSet = PublicDashboardErr{ + Reason: "No Uid for public dashboard specified", + StatusCode: 400, + } ) type PublicDashboard struct { @@ -32,7 +73,7 @@ type TimeSettings struct { // build time settings object from json on public dashboard. If empty, use // defaults on the dashboard -func (pd PublicDashboard) BuildTimeSettings(dashboard *Dashboard) *TimeSettings { +func (pd PublicDashboard) BuildTimeSettings(dashboard *models.Dashboard) *TimeSettings { ts := &TimeSettings{ From: dashboard.Data.GetPath("time", "from").MustString(), To: dashboard.Data.GetPath("time", "to").MustString(), @@ -53,6 +94,16 @@ func (pd PublicDashboard) BuildTimeSettings(dashboard *Dashboard) *TimeSettings return ts } +// +// DTO for transforming user input in the api +// +type SavePublicDashboardConfigDTO struct { + DashboardUid string + OrgId int64 + UserId int64 + PublicDashboard *PublicDashboard +} + // // COMMANDS // diff --git a/pkg/models/dashboards_public_test.go b/pkg/services/publicdashboards/models/models_test.go similarity index 84% rename from pkg/models/dashboards_public_test.go rename to pkg/services/publicdashboards/models/models_test.go index f9b18b0b1cd..b5278c247d8 100644 --- a/pkg/models/dashboards_public_test.go +++ b/pkg/services/publicdashboards/models/models_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/models" "github.com/stretchr/testify/assert" ) @@ -15,13 +16,13 @@ func TestBuildTimeSettings(t *testing.T) { var dashboardData = simplejson.NewFromAny(map[string]interface{}{"time": map[string]interface{}{"from": "now-8", "to": "now"}}) testCases := []struct { name string - dashboard *Dashboard + dashboard *models.Dashboard pubdash *PublicDashboard timeResult *TimeSettings }{ { name: "should use dashboard time if pubdash time empty", - dashboard: &Dashboard{Data: dashboardData}, + dashboard: &models.Dashboard{Data: dashboardData}, pubdash: &PublicDashboard{}, timeResult: &TimeSettings{ From: "now-8", @@ -30,7 +31,7 @@ func TestBuildTimeSettings(t *testing.T) { }, { name: "should use dashboard time if pubdash to/from empty", - dashboard: &Dashboard{Data: dashboardData}, + dashboard: &models.Dashboard{Data: dashboardData}, pubdash: &PublicDashboard{}, timeResult: &TimeSettings{ From: "now-8", @@ -39,7 +40,7 @@ func TestBuildTimeSettings(t *testing.T) { }, { name: "should use pubdash time", - dashboard: &Dashboard{Data: dashboardData}, + dashboard: &models.Dashboard{Data: dashboardData}, pubdash: &PublicDashboard{TimeSettings: simplejson.NewFromAny(map[string]interface{}{"from": "now-12", "to": "now"})}, timeResult: &TimeSettings{ From: "now-12", diff --git a/pkg/services/publicdashboards/public_dashboard_service_mock.go b/pkg/services/publicdashboards/public_dashboard_service_mock.go new file mode 100644 index 00000000000..1eb14f6c5ce --- /dev/null +++ b/pkg/services/publicdashboards/public_dashboard_service_mock.go @@ -0,0 +1,144 @@ +// Code generated by mockery v2.12.2. DO NOT EDIT. + +package publicdashboards + +import ( + context "context" + + dtos "github.com/grafana/grafana/pkg/api/dtos" + mock "github.com/stretchr/testify/mock" + + models "github.com/grafana/grafana/pkg/models" + + publicdashboardsmodels "github.com/grafana/grafana/pkg/services/publicdashboards/models" + + testing "testing" +) + +// FakePublicDashboardService is an autogenerated mock type for the Service type +type FakePublicDashboardService struct { + mock.Mock +} + +// BuildAnonymousUser provides a mock function with given fields: ctx, dashboard +func (_m *FakePublicDashboardService) BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) (*models.SignedInUser, error) { + ret := _m.Called(ctx, dashboard) + + var r0 *models.SignedInUser + if rf, ok := ret.Get(0).(func(context.Context, *models.Dashboard) *models.SignedInUser); ok { + r0 = rf(ctx, dashboard) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.SignedInUser) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *models.Dashboard) error); ok { + r1 = rf(ctx, dashboard) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BuildPublicDashboardMetricRequest provides a mock function with given fields: ctx, dashboard, publicDashboard, panelId +func (_m *FakePublicDashboardService) BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *publicdashboardsmodels.PublicDashboard, panelId int64) (dtos.MetricRequest, error) { + ret := _m.Called(ctx, dashboard, publicDashboard, panelId) + + var r0 dtos.MetricRequest + if rf, ok := ret.Get(0).(func(context.Context, *models.Dashboard, *publicdashboardsmodels.PublicDashboard, int64) dtos.MetricRequest); ok { + r0 = rf(ctx, dashboard, publicDashboard, panelId) + } else { + r0 = ret.Get(0).(dtos.MetricRequest) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *models.Dashboard, *publicdashboardsmodels.PublicDashboard, int64) error); ok { + r1 = rf(ctx, dashboard, publicDashboard, panelId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetPublicDashboard provides a mock function with given fields: ctx, accessToken +func (_m *FakePublicDashboardService) GetPublicDashboard(ctx context.Context, accessToken string) (*models.Dashboard, error) { + ret := _m.Called(ctx, accessToken) + + var r0 *models.Dashboard + if rf, ok := ret.Get(0).(func(context.Context, string) *models.Dashboard); ok { + r0 = rf(ctx, accessToken) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Dashboard) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, accessToken) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetPublicDashboardConfig provides a mock function with given fields: ctx, orgId, dashboardUid +func (_m *FakePublicDashboardService) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*publicdashboardsmodels.PublicDashboard, error) { + ret := _m.Called(ctx, orgId, dashboardUid) + + var r0 *publicdashboardsmodels.PublicDashboard + if rf, ok := ret.Get(0).(func(context.Context, int64, string) *publicdashboardsmodels.PublicDashboard); ok { + r0 = rf(ctx, orgId, dashboardUid) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*publicdashboardsmodels.PublicDashboard) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok { + r1 = rf(ctx, orgId, dashboardUid) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SavePublicDashboardConfig provides a mock function with given fields: ctx, dto +func (_m *FakePublicDashboardService) SavePublicDashboardConfig(ctx context.Context, dto *publicdashboardsmodels.SavePublicDashboardConfigDTO) (*publicdashboardsmodels.PublicDashboard, error) { + ret := _m.Called(ctx, dto) + + var r0 *publicdashboardsmodels.PublicDashboard + if rf, ok := ret.Get(0).(func(context.Context, *publicdashboardsmodels.SavePublicDashboardConfigDTO) *publicdashboardsmodels.PublicDashboard); ok { + r0 = rf(ctx, dto) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*publicdashboardsmodels.PublicDashboard) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *publicdashboardsmodels.SavePublicDashboardConfigDTO) error); ok { + r1 = rf(ctx, dto) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewFakePublicDashboardService creates a new instance of FakePublicDashboardService. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. +func NewFakePublicDashboardService(t testing.TB) *FakePublicDashboardService { + mock := &FakePublicDashboardService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/publicdashboards/public_dashboard_store_mock.go b/pkg/services/publicdashboards/public_dashboard_store_mock.go new file mode 100644 index 00000000000..20102fc1179 --- /dev/null +++ b/pkg/services/publicdashboards/public_dashboard_store_mock.go @@ -0,0 +1,142 @@ +// Code generated by mockery v2.12.2. DO NOT EDIT. + +package publicdashboards + +import ( + context "context" + + models "github.com/grafana/grafana/pkg/services/publicdashboards/models" + mock "github.com/stretchr/testify/mock" + + pkgmodels "github.com/grafana/grafana/pkg/models" + + testing "testing" +) + +// FakePublicDashboardStore is an autogenerated mock type for the Store type +type FakePublicDashboardStore struct { + mock.Mock +} + +// GenerateNewPublicDashboardUid provides a mock function with given fields: ctx +func (_m *FakePublicDashboardStore) GenerateNewPublicDashboardUid(ctx context.Context) (string, error) { + ret := _m.Called(ctx) + + var r0 string + if rf, ok := ret.Get(0).(func(context.Context) string); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(string) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetPublicDashboard provides a mock function with given fields: ctx, accessToken +func (_m *FakePublicDashboardStore) GetPublicDashboard(ctx context.Context, accessToken string) (*models.PublicDashboard, *pkgmodels.Dashboard, error) { + ret := _m.Called(ctx, accessToken) + + var r0 *models.PublicDashboard + if rf, ok := ret.Get(0).(func(context.Context, string) *models.PublicDashboard); ok { + r0 = rf(ctx, accessToken) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.PublicDashboard) + } + } + + var r1 *pkgmodels.Dashboard + if rf, ok := ret.Get(1).(func(context.Context, string) *pkgmodels.Dashboard); ok { + r1 = rf(ctx, accessToken) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*pkgmodels.Dashboard) + } + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, string) error); ok { + r2 = rf(ctx, accessToken) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// GetPublicDashboardConfig provides a mock function with given fields: ctx, orgId, dashboardUid +func (_m *FakePublicDashboardStore) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) { + ret := _m.Called(ctx, orgId, dashboardUid) + + var r0 *models.PublicDashboard + if rf, ok := ret.Get(0).(func(context.Context, int64, string) *models.PublicDashboard); ok { + r0 = rf(ctx, orgId, dashboardUid) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.PublicDashboard) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok { + r1 = rf(ctx, orgId, dashboardUid) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SavePublicDashboardConfig provides a mock function with given fields: ctx, cmd +func (_m *FakePublicDashboardStore) SavePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) (*models.PublicDashboard, error) { + ret := _m.Called(ctx, cmd) + + var r0 *models.PublicDashboard + if rf, ok := ret.Get(0).(func(context.Context, models.SavePublicDashboardConfigCommand) *models.PublicDashboard); ok { + r0 = rf(ctx, cmd) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.PublicDashboard) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, models.SavePublicDashboardConfigCommand) error); ok { + r1 = rf(ctx, cmd) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdatePublicDashboardConfig provides a mock function with given fields: ctx, cmd +func (_m *FakePublicDashboardStore) UpdatePublicDashboardConfig(ctx context.Context, cmd models.SavePublicDashboardConfigCommand) error { + ret := _m.Called(ctx, cmd) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, models.SavePublicDashboardConfigCommand) error); ok { + r0 = rf(ctx, cmd) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewFakePublicDashboardStore creates a new instance of FakePublicDashboardStore. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. +func NewFakePublicDashboardStore(t testing.TB) *FakePublicDashboardStore { + mock := &FakePublicDashboardStore{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/publicdashboards/publicdashboard.go b/pkg/services/publicdashboards/publicdashboard.go new file mode 100644 index 00000000000..4d3be2b3fa7 --- /dev/null +++ b/pkg/services/publicdashboards/publicdashboard.go @@ -0,0 +1,29 @@ +package publicdashboards + +import ( + "context" + + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/models" + . "github.com/grafana/grafana/pkg/services/publicdashboards/models" +) + +// These are the api contracts. The API should match the underlying service and store + +//go:generate mockery --name Service --structname FakePublicDashboardService --inpackage --filename public_dashboard_service_mock.go +type Service interface { + BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) (*models.SignedInUser, error) + GetPublicDashboard(ctx context.Context, accessToken string) (*models.Dashboard, error) + GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error) + SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*PublicDashboard, error) + BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64) (dtos.MetricRequest, error) +} + +//go:generate mockery --name Store --structname FakePublicDashboardStore --inpackage --filename public_dashboard_store_mock.go +type Store interface { + GetPublicDashboard(ctx context.Context, accessToken string) (*PublicDashboard, *models.Dashboard, error) + GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error) + GenerateNewPublicDashboardUid(ctx context.Context) (string, error) + SavePublicDashboardConfig(ctx context.Context, cmd SavePublicDashboardConfigCommand) (*PublicDashboard, error) + UpdatePublicDashboardConfig(ctx context.Context, cmd SavePublicDashboardConfigCommand) error +} diff --git a/pkg/services/dashboards/service/dashboard_public.go b/pkg/services/publicdashboards/service/service.go similarity index 55% rename from pkg/services/dashboards/service/dashboard_public.go rename to pkg/services/publicdashboards/service/service.go index 873c611f538..c1f69db5463 100644 --- a/pkg/services/dashboards/service/dashboard_public.go +++ b/pkg/services/publicdashboards/service/service.go @@ -8,25 +8,54 @@ import ( "github.com/google/uuid" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/publicdashboards" + . "github.com/grafana/grafana/pkg/services/publicdashboards/models" + "github.com/grafana/grafana/pkg/setting" ) +// Define the Service Implementation. We're generating mock implementation +// automatically +type PublicDashboardServiceImpl struct { + log log.Logger + cfg *setting.Cfg + store publicdashboards.Store +} + +// Gives us compile time error if the service does not adhere to the contract of +// the interface +var _ publicdashboards.Service = (*PublicDashboardServiceImpl)(nil) + +// Factory for method used by wire to inject dependencies. +// builds the service, and api, and configures routes +func ProvideService( + cfg *setting.Cfg, + store publicdashboards.Store, +) *PublicDashboardServiceImpl { + return &PublicDashboardServiceImpl{ + log: log.New("publicdashboards"), + cfg: cfg, + store: store, + } +} + // Gets public dashboard via access token -func (dr *DashboardServiceImpl) GetPublicDashboard(ctx context.Context, accessToken string) (*models.Dashboard, error) { - pubdash, d, err := dr.dashboardStore.GetPublicDashboard(ctx, accessToken) +func (pd *PublicDashboardServiceImpl) GetPublicDashboard(ctx context.Context, accessToken string) (*models.Dashboard, error) { + pubdash, d, err := pd.store.GetPublicDashboard(ctx, accessToken) if err != nil { return nil, err } if pubdash == nil || d == nil { - return nil, dashboards.ErrPublicDashboardNotFound + return nil, ErrPublicDashboardNotFound } if !pubdash.IsEnabled { - return nil, dashboards.ErrPublicDashboardNotFound + return nil, ErrPublicDashboardNotFound } ts := pubdash.BuildTimeSettings(d) @@ -37,8 +66,8 @@ func (dr *DashboardServiceImpl) GetPublicDashboard(ctx context.Context, accessTo } // GetPublicDashboardConfig is a helper method to retrieve the public dashboard configuration for a given dashboard from the database -func (dr *DashboardServiceImpl) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboard, error) { - pdc, err := dr.dashboardStore.GetPublicDashboardConfig(ctx, orgId, dashboardUid) +func (pd *PublicDashboardServiceImpl) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error) { + pdc, err := pd.store.GetPublicDashboardConfig(ctx, orgId, dashboardUid) if err != nil { return nil, err } @@ -48,7 +77,7 @@ func (dr *DashboardServiceImpl) GetPublicDashboardConfig(ctx context.Context, or // SavePublicDashboardConfig is a helper method to persist the sharing config // to the database. It handles validations for sharing config and persistence -func (dr *DashboardServiceImpl) SavePublicDashboardConfig(ctx context.Context, dto *dashboards.SavePublicDashboardConfigDTO) (*models.PublicDashboard, error) { +func (pd *PublicDashboardServiceImpl) SavePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*PublicDashboard, error) { if len(dto.DashboardUid) == 0 { return nil, dashboards.ErrDashboardIdentifierNotSet } @@ -59,14 +88,14 @@ func (dr *DashboardServiceImpl) SavePublicDashboardConfig(ctx context.Context, d } if dto.PublicDashboard.Uid == "" { - return dr.savePublicDashboardConfig(ctx, dto) + return pd.savePublicDashboardConfig(ctx, dto) } - return dr.updatePublicDashboardConfig(ctx, dto) + return pd.updatePublicDashboardConfig(ctx, dto) } -func (dr *DashboardServiceImpl) savePublicDashboardConfig(ctx context.Context, dto *dashboards.SavePublicDashboardConfigDTO) (*models.PublicDashboard, error) { - uid, err := dr.dashboardStore.GenerateNewPublicDashboardUid(ctx) +func (pd *PublicDashboardServiceImpl) savePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*PublicDashboard, error) { + uid, err := pd.store.GenerateNewPublicDashboardUid(ctx) if err != nil { return nil, err } @@ -76,10 +105,10 @@ func (dr *DashboardServiceImpl) savePublicDashboardConfig(ctx context.Context, d return nil, err } - cmd := models.SavePublicDashboardConfigCommand{ + cmd := SavePublicDashboardConfigCommand{ DashboardUid: dto.DashboardUid, OrgId: dto.OrgId, - PublicDashboard: models.PublicDashboard{ + PublicDashboard: PublicDashboard{ Uid: uid, DashboardUid: dto.DashboardUid, OrgId: dto.OrgId, @@ -91,12 +120,12 @@ func (dr *DashboardServiceImpl) savePublicDashboardConfig(ctx context.Context, d }, } - return dr.dashboardStore.SavePublicDashboardConfig(ctx, cmd) + return pd.store.SavePublicDashboardConfig(ctx, cmd) } -func (dr *DashboardServiceImpl) updatePublicDashboardConfig(ctx context.Context, dto *dashboards.SavePublicDashboardConfigDTO) (*models.PublicDashboard, error) { - cmd := models.SavePublicDashboardConfigCommand{ - PublicDashboard: models.PublicDashboard{ +func (pd *PublicDashboardServiceImpl) updatePublicDashboardConfig(ctx context.Context, dto *SavePublicDashboardConfigDTO) (*PublicDashboard, error) { + cmd := SavePublicDashboardConfigCommand{ + PublicDashboard: PublicDashboard{ Uid: dto.PublicDashboard.Uid, IsEnabled: dto.PublicDashboard.IsEnabled, TimeSettings: dto.PublicDashboard.TimeSettings, @@ -105,12 +134,12 @@ func (dr *DashboardServiceImpl) updatePublicDashboardConfig(ctx context.Context, }, } - err := dr.dashboardStore.UpdatePublicDashboardConfig(ctx, cmd) + err := pd.store.UpdatePublicDashboardConfig(ctx, cmd) if err != nil { return nil, err } - publicDashboard, err := dr.dashboardStore.GetPublicDashboardConfig(ctx, dto.OrgId, dto.DashboardUid) + publicDashboard, err := pd.store.GetPublicDashboardConfig(ctx, dto.OrgId, dto.DashboardUid) if err != nil { return nil, err } @@ -120,15 +149,15 @@ func (dr *DashboardServiceImpl) updatePublicDashboardConfig(ctx context.Context, // BuildPublicDashboardMetricRequest merges public dashboard parameters with // dashboard and returns a metrics request to be sent to query backend -func (dr *DashboardServiceImpl) BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *models.PublicDashboard, panelId int64) (dtos.MetricRequest, error) { +func (pd *PublicDashboardServiceImpl) BuildPublicDashboardMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64) (dtos.MetricRequest, error) { if !publicDashboard.IsEnabled { - return dtos.MetricRequest{}, dashboards.ErrPublicDashboardNotFound + return dtos.MetricRequest{}, ErrPublicDashboardNotFound } queriesByPanel := models.GroupQueriesByPanelId(dashboard.Data) if _, ok := queriesByPanel[panelId]; !ok { - return dtos.MetricRequest{}, dashboards.ErrPublicDashboardPanelNotFound + return dtos.MetricRequest{}, ErrPublicDashboardPanelNotFound } ts := publicDashboard.BuildTimeSettings(dashboard) @@ -141,7 +170,7 @@ func (dr *DashboardServiceImpl) BuildPublicDashboardMetricRequest(ctx context.Co } // BuildAnonymousUser creates a user with permissions to read from all datasources used in the dashboard -func (dr *DashboardServiceImpl) BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) (*models.SignedInUser, error) { +func (pd *PublicDashboardServiceImpl) BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) (*models.SignedInUser, error) { datasourceUids := models.GetUniqueDashboardDatasourceUids(dashboard.Data) // Create a temp user with read-only datasource permissions diff --git a/pkg/services/dashboards/service/dashboard_public_test.go b/pkg/services/publicdashboards/service/service_test.go similarity index 81% rename from pkg/services/dashboards/service/dashboard_public_test.go rename to pkg/services/publicdashboards/service/service_test.go index 8da35c5c202..8198d41f4fa 100644 --- a/pkg/services/dashboards/service/dashboard_public_test.go +++ b/pkg/services/publicdashboards/service/service_test.go @@ -13,8 +13,10 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/dashboards/database" + dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database" + . "github.com/grafana/grafana/pkg/services/publicdashboards" + database "github.com/grafana/grafana/pkg/services/publicdashboards/database" + . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/sqlstore" ) @@ -25,7 +27,7 @@ var mergedDashboardData = simplejson.NewFromAny(map[string]interface{}{"time": m func TestGetPublicDashboard(t *testing.T) { type storeResp struct { - pd *models.PublicDashboard + pd *PublicDashboard d *models.Dashboard err error } @@ -41,7 +43,7 @@ func TestGetPublicDashboard(t *testing.T) { Name: "returns a dashboard", AccessToken: "abc123", StoreResp: &storeResp{ - pd: &models.PublicDashboard{IsEnabled: true}, + pd: &PublicDashboard{IsEnabled: true}, d: &models.Dashboard{Uid: "mydashboard", Data: dashboardData}, err: nil, }, @@ -52,7 +54,7 @@ func TestGetPublicDashboard(t *testing.T) { Name: "puts pubdash time settings into dashboard", AccessToken: "abc123", StoreResp: &storeResp{ - pd: &models.PublicDashboard{IsEnabled: true, TimeSettings: timeSettings}, + pd: &PublicDashboard{IsEnabled: true, TimeSettings: timeSettings}, d: &models.Dashboard{Data: dashboardData}, err: nil, }, @@ -63,35 +65,35 @@ func TestGetPublicDashboard(t *testing.T) { Name: "returns ErrPublicDashboardNotFound when isEnabled is false", AccessToken: "abc123", StoreResp: &storeResp{ - pd: &models.PublicDashboard{IsEnabled: false}, + pd: &PublicDashboard{IsEnabled: false}, d: &models.Dashboard{Uid: "mydashboard"}, err: nil, }, - ErrResp: dashboards.ErrPublicDashboardNotFound, + ErrResp: ErrPublicDashboardNotFound, DashResp: nil, }, { Name: "returns ErrPublicDashboardNotFound if PublicDashboard missing", AccessToken: "abc123", StoreResp: &storeResp{pd: nil, d: nil, err: nil}, - ErrResp: dashboards.ErrPublicDashboardNotFound, + ErrResp: ErrPublicDashboardNotFound, DashResp: nil, }, { Name: "returns ErrPublicDashboardNotFound if Dashboard missing", AccessToken: "abc123", StoreResp: &storeResp{pd: nil, d: nil, err: nil}, - ErrResp: dashboards.ErrPublicDashboardNotFound, + ErrResp: ErrPublicDashboardNotFound, DashResp: nil, }, } for _, test := range testCases { t.Run(test.Name, func(t *testing.T) { - fakeStore := dashboards.FakeDashboardStore{} - service := &DashboardServiceImpl{ - log: log.New("test.logger"), - dashboardStore: &fakeStore, + fakeStore := FakePublicDashboardStore{} + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: &fakeStore, } fakeStore.On("GetPublicDashboard", mock.Anything, mock.Anything). @@ -116,19 +118,20 @@ func TestGetPublicDashboard(t *testing.T) { func TestSavePublicDashboard(t *testing.T) { t.Run("Saving public dashboard", func(t *testing.T) { sqlStore := sqlstore.InitTestDB(t) - dashboardStore := database.ProvideDashboardStore(sqlStore) + dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore) + publicdashboardStore := database.ProvideStore(sqlStore) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) - service := &DashboardServiceImpl{ - log: log.New("test.logger"), - dashboardStore: dashboardStore, + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: publicdashboardStore, } - dto := &dashboards.SavePublicDashboardConfigDTO{ + dto := &SavePublicDashboardConfigDTO{ DashboardUid: dashboard.Uid, OrgId: dashboard.OrgId, UserId: 7, - PublicDashboard: &models.PublicDashboard{ + PublicDashboard: &PublicDashboard{ IsEnabled: true, DashboardUid: "NOTTHESAME", OrgId: 9999999, @@ -159,19 +162,20 @@ func TestSavePublicDashboard(t *testing.T) { t.Run("Validate pubdash has default time setting value", func(t *testing.T) { sqlStore := sqlstore.InitTestDB(t) - dashboardStore := database.ProvideDashboardStore(sqlStore) + dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore) + publicdashboardStore := database.ProvideStore(sqlStore) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) - service := &DashboardServiceImpl{ - log: log.New("test.logger"), - dashboardStore: dashboardStore, + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: publicdashboardStore, } - dto := &dashboards.SavePublicDashboardConfigDTO{ + dto := &SavePublicDashboardConfigDTO{ DashboardUid: dashboard.Uid, OrgId: dashboard.OrgId, UserId: 7, - PublicDashboard: &models.PublicDashboard{ + PublicDashboard: &PublicDashboard{ IsEnabled: true, DashboardUid: "NOTTHESAME", OrgId: 9999999, @@ -192,19 +196,20 @@ func TestSavePublicDashboard(t *testing.T) { func TestUpdatePublicDashboard(t *testing.T) { t.Run("Updating public dashboard", func(t *testing.T) { sqlStore := sqlstore.InitTestDB(t) - dashboardStore := database.ProvideDashboardStore(sqlStore) + dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore) + publicdashboardStore := database.ProvideStore(sqlStore) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) - service := &DashboardServiceImpl{ - log: log.New("test.logger"), - dashboardStore: dashboardStore, + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: publicdashboardStore, } - dto := &dashboards.SavePublicDashboardConfigDTO{ + dto := &SavePublicDashboardConfigDTO{ DashboardUid: dashboard.Uid, OrgId: dashboard.OrgId, UserId: 7, - PublicDashboard: &models.PublicDashboard{ + PublicDashboard: &PublicDashboard{ IsEnabled: true, TimeSettings: timeSettings, }, @@ -217,11 +222,11 @@ func TestUpdatePublicDashboard(t *testing.T) { require.NoError(t, err) // attempt to overwrite settings - dto = &dashboards.SavePublicDashboardConfigDTO{ + dto = &SavePublicDashboardConfigDTO{ DashboardUid: dashboard.Uid, OrgId: dashboard.OrgId, UserId: 8, - PublicDashboard: &models.PublicDashboard{ + PublicDashboard: &PublicDashboard{ Uid: savedPubdash.Uid, OrgId: 9, DashboardUid: "abc1234", @@ -258,19 +263,20 @@ func TestUpdatePublicDashboard(t *testing.T) { t.Run("Updating set empty time settings", func(t *testing.T) { sqlStore := sqlstore.InitTestDB(t) - dashboardStore := database.ProvideDashboardStore(sqlStore) + dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore) + publicdashboardStore := database.ProvideStore(sqlStore) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) - service := &DashboardServiceImpl{ - log: log.New("test.logger"), - dashboardStore: dashboardStore, + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: publicdashboardStore, } - dto := &dashboards.SavePublicDashboardConfigDTO{ + dto := &SavePublicDashboardConfigDTO{ DashboardUid: dashboard.Uid, OrgId: dashboard.OrgId, UserId: 7, - PublicDashboard: &models.PublicDashboard{ + PublicDashboard: &PublicDashboard{ IsEnabled: true, TimeSettings: timeSettings, }, @@ -285,11 +291,11 @@ func TestUpdatePublicDashboard(t *testing.T) { require.NoError(t, err) // attempt to overwrite settings - dto = &dashboards.SavePublicDashboardConfigDTO{ + dto = &SavePublicDashboardConfigDTO{ DashboardUid: dashboard.Uid, OrgId: dashboard.OrgId, UserId: 8, - PublicDashboard: &models.PublicDashboard{ + PublicDashboard: &PublicDashboard{ Uid: savedPubdash.Uid, OrgId: 9, DashboardUid: "abc1234", @@ -316,11 +322,12 @@ func TestUpdatePublicDashboard(t *testing.T) { func TestBuildAnonymousUser(t *testing.T) { sqlStore := sqlstore.InitTestDB(t) - dashboardStore := database.ProvideDashboardStore(sqlStore) + dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore) dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) - service := &DashboardServiceImpl{ - log: log.New("test.logger"), - dashboardStore: dashboardStore, + publicdashboardStore := database.ProvideStore(sqlStore) + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: publicdashboardStore, } t.Run("will add datasource read and query permissions to user for each datasource in dashboard", func(t *testing.T) { @@ -336,19 +343,21 @@ func TestBuildAnonymousUser(t *testing.T) { func TestBuildPublicDashboardMetricRequest(t *testing.T) { sqlStore := sqlstore.InitTestDB(t) - dashboardStore := database.ProvideDashboardStore(sqlStore) + dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore) + publicdashboardStore := database.ProvideStore(sqlStore) + publicDashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) nonPublicDashboard := insertTestDashboard(t, dashboardStore, "testNonPublicDashie", 1, 0, true) - service := &DashboardServiceImpl{ - log: log.New("test.logger"), - dashboardStore: dashboardStore, + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: publicdashboardStore, } - dto := &dashboards.SavePublicDashboardConfigDTO{ + dto := &SavePublicDashboardConfigDTO{ DashboardUid: publicDashboard.Uid, OrgId: publicDashboard.OrgId, - PublicDashboard: &models.PublicDashboard{ + PublicDashboard: &PublicDashboard{ IsEnabled: true, DashboardUid: "NOTTHESAME", OrgId: 9999999, @@ -359,10 +368,10 @@ func TestBuildPublicDashboardMetricRequest(t *testing.T) { publicDashboardPD, err := service.SavePublicDashboardConfig(context.Background(), dto) require.NoError(t, err) - nonPublicDto := &dashboards.SavePublicDashboardConfigDTO{ + nonPublicDto := &SavePublicDashboardConfigDTO{ DashboardUid: nonPublicDashboard.Uid, OrgId: nonPublicDashboard.OrgId, - PublicDashboard: &models.PublicDashboard{ + PublicDashboard: &PublicDashboard{ IsEnabled: false, DashboardUid: "NOTTHESAME", OrgId: 9999999, @@ -431,7 +440,7 @@ func TestBuildPublicDashboardMetricRequest(t *testing.T) { }) } -func insertTestDashboard(t *testing.T, dashboardStore *database.DashboardStore, title string, orgId int64, +func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardStore, title string, orgId int64, folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard { t.Helper() cmd := models.SaveDashboardCommand{