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{