From 0b4af38bfa087967bebbac057e8e9f8375da9476 Mon Sep 17 00:00:00 2001 From: owensmallwood Date: Wed, 6 Jul 2022 12:42:39 -0600 Subject: [PATCH] Public Dashboards: Query Caching (#51403) * passes id and uid to PublicDashboardDatasource * betterer results * If for a public dashboard, return the PublicDashboardDataSource first or else getDatasourceSrv.get() will fail bc of no authed user. Added some unit tests for resolving the uid from the many possible datasource types. * updates betterer * Exports DashboardService. Adds method to DashboardService to build anonymous user for use with public dashboards where there is no authed user. Adds method on dashboard_queries to get all dashboard uids from a dashboard. * refactors to get unique datasource uids * Adds tests for getting all unique datasource uids off a dashboard * adds test for building anonymous user with read and query actions that are scoped to each datasource uid in the dashboard * updates casing of DashboardService * updates test case to have additional panel with a different datasource * gives default interval to public dashboard data source --- .betterer.results | 56 +++++++++- packages/grafana-data/src/types/datasource.ts | 1 - .../utils/PublicDashboardDataSource.test.ts | 34 +++++- pkg/api/annotations.go | 6 +- pkg/api/annotations_test.go | 4 +- pkg/api/api.go | 2 +- pkg/api/common_test.go | 2 +- pkg/api/dashboard.go | 18 +-- pkg/api/dashboard_permission.go | 2 +- pkg/api/dashboard_permission_test.go | 2 +- pkg/api/dashboard_public.go | 12 +- pkg/api/dashboard_public_test.go | 12 +- pkg/api/dashboard_test.go | 14 +-- pkg/api/dtos/models.go | 2 + pkg/api/folder_permission.go | 2 +- pkg/api/folder_permission_test.go | 2 +- pkg/api/http_server.go | 4 +- pkg/api/index.go | 4 +- pkg/api/playlist_play.go | 2 +- pkg/api/preferences.go | 8 +- pkg/api/preferences_test.go | 4 +- pkg/api/stars.go | 2 +- pkg/models/dashboard_queries.go | 18 ++- pkg/models/dashboard_queries_test.go | 103 +++++++++++++++++- pkg/services/dashboards/dashboard.go | 1 + .../dashboards/dashboard_service_mock.go | 23 ++++ .../dashboards/service/dashboard_public.go | 23 +++- .../service/dashboard_public_test.go | 26 +++++ .../services/PublicDashboardDataSource.ts | 30 ++++- .../features/query/state/PanelQueryRunner.ts | 2 +- 30 files changed, 353 insertions(+), 68 deletions(-) diff --git a/.betterer.results b/.betterer.results index f8a8a7e54df..c8cfa3c4570 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1,5 +1,5 @@ // BETTERER RESULTS V2. -// +// // If this file contains merge conflicts, use `betterer merge` to automatically resolve them: // https://phenomnomnominal.github.io/betterer/docs/results-file/#merge // @@ -131,6 +131,37 @@ exports[`better eslint`] = { "e2e/dashboards-suite/dashboard-templating.spec.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], + "packages/grafana-data/src/types/datasource.ts:1730680024": [ + [24, 85, 3, "Unexpected any. Specify a different type.", "193409811"], + [27, 73, 3, "Unexpected any. Specify a different type.", "193409811"], + [46, 28, 3, "Unexpected any. Specify a different type.", "193409811"], + [51, 26, 3, "Unexpected any. Specify a different type.", "193409811"], + [56, 47, 3, "Unexpected any. Specify a different type.", "193409811"], + [99, 46, 3, "Unexpected any. Specify a different type.", "193409811"], + [109, 48, 3, "Unexpected any. Specify a different type.", "193409811"], + [151, 14, 3, "Unexpected any. Specify a different type.", "193409811"], + [152, 25, 3, "Unexpected any. Specify a different type.", "193409811"], + [153, 24, 3, "Unexpected any. Specify a different type.", "193409811"], + [172, 72, 3, "Unexpected any. Specify a different type.", "193409811"], + [246, 37, 3, "Unexpected any. Specify a different type.", "193409811"], + [260, 41, 3, "Unexpected any. Specify a different type.", "193409811"], + [260, 57, 3, "Unexpected any. Specify a different type.", "193409811"], + [270, 26, 3, "Unexpected any. Specify a different type.", "193409811"], + [270, 41, 3, "Unexpected any. Specify a different type.", "193409811"], + [275, 24, 3, "Unexpected any. Specify a different type.", "193409811"], + [280, 25, 3, "Unexpected any. Specify a different type.", "193409811"], + [317, 21, 3, "Unexpected any. Specify a different type.", "193409811"], + [319, 32, 3, "Unexpected any. Specify a different type.", "193409811"], + [382, 14, 3, "Unexpected any. Specify a different type.", "193409811"], + [408, 14, 3, "Unexpected any. Specify a different type.", "193409811"], + [414, 58, 3, "Unexpected any. Specify a different type.", "193409811"], + [608, 13, 3, "Unexpected any. Specify a different type.", "193409811"], + [618, 37, 3, "Unexpected any. Specify a different type.", "193409811"], + [618, 42, 3, "Unexpected any. Specify a different type.", "193409811"], + [619, 43, 3, "Unexpected any. Specify a different type.", "193409811"], + [619, 59, 3, "Unexpected any. Specify a different type.", "193409811"], + [625, 46, 3, "Unexpected any. Specify a different type.", "193409811"], + [626, 22, 3, "Unexpected any. Specify a different type.", "193409811"] "e2e/dashboards-suite/textbox-variables.spec.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -693,6 +724,11 @@ exports[`better eslint`] = { "packages/grafana-data/src/types/flot.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], + "packages/grafana-runtime/src/utils/PublicDashboardDataSource.test.ts:2092121707": [ + [14, 19, 123, "Do not use any type assertions.", "3028355264"], + [14, 19, 109, "Do not use any type assertions.", "4248357345"], + [39, 13, 206, "Do not use any type assertions.", "1200376833"], + [72, 49, 54, "Do not use any type assertions.", "114713672"] "packages/grafana-data/src/types/graph.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -4333,6 +4369,13 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "5"], [0, 0, 0, "Unexpected any. Specify a different type.", "6"] ], + "public/app/features/dashboard/services/PublicDashboardDataSource.ts:102072381": [ + [14, 61, 3, "Unexpected any. Specify a different type.", "193409811"], + [20, 12, 16, "Do not use any type assertions.", "1747412709"], + [43, 34, 3, "Unexpected any. Specify a different type.", "193409811"], + [61, 16, 3, "Unexpected any. Specify a different type.", "193409811"], + [78, 45, 22, "Do not use any type assertions.", "1838499175"], + [86, 28, 3, "Unexpected any. Specify a different type.", "193409811"] "public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -5639,6 +5682,17 @@ exports[`better eslint`] = { "public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], + "public/app/features/query/state/PanelQueryRunner.ts:3311920590": [ + [110, 39, 118, "Do not use any type assertions.", "2873233307"], + [189, 46, 3, "Unexpected any. Specify a different type.", "193409811"], + [238, 5, 14, "Do not use any type assertions.", "4095749936"], + [238, 16, 3, "Unexpected any. Specify a different type.", "193409811"], + [285, 20, 46, "Do not use any type assertions.", "1712789723"], + [285, 20, 32, "Do not use any type assertions.", "2220885232"], + [367, 21, 17, "Do not use any type assertions.", "1733699692"], + [367, 35, 3, "Unexpected any. Specify a different type.", "193409811"], + [368, 11, 27, "Do not use any type assertions.", "2133479311"], + [371, 38, 20, "Do not use any type assertions.", "340150831"] "public/app/features/query/state/DashboardQueryRunner/LegacyAnnotationQueryRunner.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index 3b118ec2efb..1be984a4dbd 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -483,7 +483,6 @@ export interface DataQueryRequest { timeInfo?: string; // The query time description (blue text in the upper right) panelId?: number; dashboardId?: number; - // Temporary prop for public dashboards, to be replaced by publicAccessKey publicDashboardAccessToken?: string; // Request Timing diff --git a/packages/grafana-runtime/src/utils/PublicDashboardDataSource.test.ts b/packages/grafana-runtime/src/utils/PublicDashboardDataSource.test.ts index c86ee702e7c..f72460483f8 100644 --- a/packages/grafana-runtime/src/utils/PublicDashboardDataSource.test.ts +++ b/packages/grafana-runtime/src/utils/PublicDashboardDataSource.test.ts @@ -1,9 +1,14 @@ import { of } from 'rxjs'; import { BackendSrv, BackendSrvRequest } from 'src/services'; -import { DataQueryRequest, DataSourceRef } from '@grafana/data'; +import { DataQueryRequest, DataSourceInstanceSettings, DataSourceRef } from '@grafana/data'; -import { PublicDashboardDataSource } from '../../../../public/app/features/dashboard/services/PublicDashboardDataSource'; +import { + PUBLIC_DATASOURCE, + PublicDashboardDataSource, +} from '../../../../public/app/features/dashboard/services/PublicDashboardDataSource'; + +import { DataSourceWithBackend } from './DataSourceWithBackend'; const mockDatasourceRequest = jest.fn(); @@ -28,7 +33,7 @@ describe('PublicDashboardDatasource', () => { mockDatasourceRequest.mockReset(); mockDatasourceRequest.mockReturnValue(Promise.resolve({})); - const ds = new PublicDashboardDataSource(); + const ds = new PublicDashboardDataSource('public'); const panelId = 1; const publicDashboardAccessToken = 'abc123'; @@ -47,4 +52,27 @@ describe('PublicDashboardDatasource', () => { `/api/public/dashboards/${publicDashboardAccessToken}/panels/${panelId}/query` ); }); + + test('returns public datasource uid when datasource passed in is null', () => { + let ds = new PublicDashboardDataSource(null); + expect(ds.uid).toBe(PUBLIC_DATASOURCE); + }); + + test('returns datasource when datasource passed in is a string', () => { + let ds = new PublicDashboardDataSource('theDatasourceUid'); + expect(ds.uid).toBe('theDatasourceUid'); + }); + + test('returns datasource uid when datasource passed in is a DataSourceRef implementation', () => { + const datasource = { type: 'datasource', uid: 'abc123' }; + let ds = new PublicDashboardDataSource(datasource); + expect(ds.uid).toBe('abc123'); + }); + + test('returns datasource uid when datasource passed in is a DatasourceApi instance', () => { + const settings: DataSourceInstanceSettings = { id: 1, uid: 'abc123' } as DataSourceInstanceSettings; + const datasource = new DataSourceWithBackend(settings); + let ds = new PublicDashboardDataSource(datasource); + expect(ds.uid).toBe('abc123'); + }); }); diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index 492b87af29b..894f4382ead 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -53,7 +53,7 @@ func (hs *HTTPServer) GetAnnotations(c *models.ReqContext) response.Response { item.DashboardUID = val } else { query := models.GetDashboardQuery{Id: item.DashboardId, OrgId: c.OrgId} - err := hs.dashboardService.GetDashboard(c.Req.Context(), &query) + err := hs.DashboardService.GetDashboard(c.Req.Context(), &query) if err == nil && query.Result != nil { item.DashboardUID = &query.Result.Uid dashboardCache[item.DashboardId] = &query.Result.Uid @@ -82,7 +82,7 @@ func (hs *HTTPServer) PostAnnotation(c *models.ReqContext) response.Response { // overwrite dashboardId when dashboardUID is not empty if cmd.DashboardUID != "" { query := models.GetDashboardQuery{OrgId: c.OrgId, Uid: cmd.DashboardUID} - err := hs.dashboardService.GetDashboard(c.Req.Context(), &query) + err := hs.DashboardService.GetDashboard(c.Req.Context(), &query) if err == nil { cmd.DashboardId = query.Result.Id } @@ -291,7 +291,7 @@ func (hs *HTTPServer) MassDeleteAnnotations(c *models.ReqContext) response.Respo if cmd.DashboardUID != "" { query := models.GetDashboardQuery{OrgId: c.OrgId, Uid: cmd.DashboardUID} - err := hs.dashboardService.GetDashboard(c.Req.Context(), &query) + err := hs.DashboardService.GetDashboard(c.Req.Context(), &query) if err == nil { cmd.DashboardId = query.Result.Id } diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index 8b97423c59d..c2bbd3baaeb 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -343,7 +343,7 @@ func postAnnotationScenario(t *testing.T, desc string, url string, routePattern t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { hs := setupSimpleHTTPServer(nil) hs.SQLStore = store - hs.dashboardService = dashSvc + hs.DashboardService = dashSvc sc := setupScenarioContext(t, url) sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { @@ -428,7 +428,7 @@ func deleteAnnotationsScenario(t *testing.T, desc string, url string, routePatte t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { hs := setupSimpleHTTPServer(nil) hs.SQLStore = store - hs.dashboardService = dashSvc + hs.DashboardService = dashSvc sc := setupScenarioContext(t, url) sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { diff --git a/pkg/api/api.go b/pkg/api/api.go index 57fec6b43e3..fbe8e735e97 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -28,7 +28,7 @@ func (hs *HTTPServer) registerRoutes() { reqGrafanaAdmin := middleware.ReqGrafanaAdmin reqEditorRole := middleware.ReqEditorRole reqOrgAdmin := middleware.ReqOrgAdmin - reqOrgAdminDashOrFolderAdminOrTeamAdmin := middleware.OrgAdminDashOrFolderAdminOrTeamAdmin(hs.SQLStore, hs.dashboardService) + reqOrgAdminDashOrFolderAdminOrTeamAdmin := middleware.OrgAdminDashOrFolderAdminOrTeamAdmin(hs.SQLStore, hs.DashboardService) reqCanAccessTeams := middleware.AdminOrEditorAndFeatureEnabled(hs.Cfg.EditorsCanAdmin) reqSnapshotPublicModeOrSignedIn := middleware.SnapshotPublicModeOrSignedIn(hs.Cfg) redirectFromLegacyPanelEditURL := middleware.RedirectFromLegacyPanelEditURL(hs.Cfg) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index e28a0758726..b5b061a4942 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -396,7 +396,7 @@ func setupHTTPServerWithCfgDb(t *testing.T, useFakeAccessControl, enableAccessCo AccessControl: ac, teamPermissionsService: teamPermissionService, searchUsersService: searchusers.ProvideUsersService(db, filters.ProvideOSSSearchUserFilter()), - dashboardService: dashboardservice.ProvideDashboardService( + DashboardService: dashboardservice.ProvideDashboardService( cfg, dashboardsStore, nil, features, accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.NewMockedPermissionsService(), ac, ), diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 8b07440df31..95b88909641 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -144,7 +144,7 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { // lookup folder title if dash.FolderId > 0 { query := models.GetDashboardQuery{Id: dash.FolderId, OrgId: c.OrgId} - if err := hs.dashboardService.GetDashboard(c.Req.Context(), &query); err != nil { + if err := hs.DashboardService.GetDashboard(c.Req.Context(), &query); err != nil { if errors.Is(err, dashboards.ErrFolderNotFound) { return response.Error(404, "Folder not found", err) } @@ -235,7 +235,7 @@ func (hs *HTTPServer) getDashboardHelper(ctx context.Context, orgID int64, id in query = models.GetDashboardQuery{Id: id, OrgId: orgID} } - if err := hs.dashboardService.GetDashboard(ctx, &query); err != nil { + if err := hs.DashboardService.GetDashboard(ctx, &query); err != nil { return nil, response.Error(404, "Dashboard not found", err) } @@ -262,7 +262,7 @@ func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response { hs.log.Error("Failed to disconnect library elements", "dashboard", dash.Id, "user", c.SignedInUser.UserId, "error", err) } - err = hs.dashboardService.DeleteDashboard(c.Req.Context(), dash.Id, c.OrgId) + err = hs.DashboardService.DeleteDashboard(c.Req.Context(), dash.Id, c.OrgId) if err != nil { var dashboardErr dashboards.DashboardErr if ok := errors.As(err, &dashboardErr); ok { @@ -387,7 +387,7 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa Overwrite: cmd.Overwrite, } - dashboard, err := hs.dashboardService.SaveDashboard(alerting.WithUAEnabled(ctx, hs.Cfg.UnifiedAlerting.IsEnabled()), dashItem, allowUiUpdate) + dashboard, err := hs.DashboardService.SaveDashboard(alerting.WithUAEnabled(ctx, hs.Cfg.UnifiedAlerting.IsEnabled()), dashItem, allowUiUpdate) if dashboard != nil && hs.entityEventsService != nil { if err := hs.entityEventsService.SaveEvent(ctx, store.SaveEventCmd{ @@ -460,7 +460,7 @@ func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) response.Response { if preference.HomeDashboardID != 0 { slugQuery := models.GetDashboardRefByIdQuery{Id: preference.HomeDashboardID} - err := hs.dashboardService.GetDashboardUIDById(c.Req.Context(), &slugQuery) + err := hs.DashboardService.GetDashboardUIDById(c.Req.Context(), &slugQuery) if err == nil { url := models.GetDashboardUrl(slugQuery.Result.Uid, slugQuery.Result.Slug) dashRedirect := dtos.DashboardRedirect{RedirectUri: url} @@ -546,7 +546,7 @@ func (hs *HTTPServer) GetDashboardVersions(c *models.ReqContext) response.Respon OrgId: c.SignedInUser.OrgId, Uid: dashUID, } - if err := hs.dashboardService.GetDashboard(c.Req.Context(), &q); err != nil { + if err := hs.DashboardService.GetDashboard(c.Req.Context(), &q); err != nil { return response.Error(http.StatusBadRequest, "failed to get dashboard by UID", err) } dashID = q.Result.Id @@ -605,7 +605,7 @@ func (hs *HTTPServer) GetDashboardVersion(c *models.ReqContext) response.Respons OrgId: c.SignedInUser.OrgId, Uid: dashUID, } - if err := hs.dashboardService.GetDashboard(c.Req.Context(), &q); err != nil { + if err := hs.DashboardService.GetDashboard(c.Req.Context(), &q); err != nil { return response.Error(http.StatusBadRequest, "failed to get dashboard by UID", err) } dashID = q.Result.Id @@ -782,7 +782,7 @@ func (hs *HTTPServer) RestoreDashboardVersion(c *models.ReqContext) response.Res func (hs *HTTPServer) GetDashboardTags(c *models.ReqContext) { query := models.GetDashboardTagsQuery{OrgId: c.OrgId} - err := hs.dashboardService.GetDashboardTags(c.Req.Context(), &query) + err := hs.DashboardService.GetDashboardTags(c.Req.Context(), &query) if err != nil { c.JsonApiErr(500, "Failed to get tags from database", err) return @@ -803,7 +803,7 @@ func (hs *HTTPServer) GetDashboardUIDs(c *models.ReqContext) { continue } q.Id = id - err = hs.dashboardService.GetDashboardUIDById(c.Req.Context(), q) + err = hs.DashboardService.GetDashboardUIDById(c.Req.Context(), q) if err != nil { continue } diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go index 4ae1ce4d36e..6bbec530a18 100644 --- a/pkg/api/dashboard_permission.go +++ b/pkg/api/dashboard_permission.go @@ -143,7 +143,7 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext) response. return response.Success("Dashboard permissions updated") } - if err := hs.dashboardService.UpdateDashboardACL(c.Req.Context(), dashID, items); err != nil { + if err := hs.DashboardService.UpdateDashboardACL(c.Req.Context(), dashID, items); err != nil { if errors.Is(err, models.ErrDashboardAclInfoMissing) || errors.Is(err, models.ErrDashboardPermissionDashboardEmpty) { return response.Error(409, err.Error(), err) diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index 14cc1abc5cf..0b15fee9012 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -39,7 +39,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { Cfg: settings, SQLStore: mockSQLStore, Features: features, - dashboardService: dashboardservice.ProvideDashboardService( + DashboardService: dashboardservice.ProvideDashboardService( settings, dashboardStore, nil, features, folderPermissions, dashboardPermissions, ac, ), AccessControl: accesscontrolmock.New().WithDisabled(), diff --git a/pkg/api/dashboard_public.go b/pkg/api/dashboard_public.go index 9850a9cee5d..b23108c3725 100644 --- a/pkg/api/dashboard_public.go +++ b/pkg/api/dashboard_public.go @@ -19,7 +19,7 @@ import ( func (hs *HTTPServer) GetPublicDashboard(c *models.ReqContext) response.Response { accessToken := web.Params(c.Req)[":accessToken"] - dash, err := hs.dashboardService.GetPublicDashboard(c.Req.Context(), accessToken) + dash, err := hs.DashboardService.GetPublicDashboard(c.Req.Context(), accessToken) if err != nil { return handleDashboardErr(http.StatusInternalServerError, "Failed to get public dashboard", err) } @@ -47,7 +47,7 @@ func (hs *HTTPServer) GetPublicDashboard(c *models.ReqContext) response.Response // 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"]) + 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) } @@ -71,7 +71,7 @@ func (hs *HTTPServer) SavePublicDashboardConfig(c *models.ReqContext) response.R PublicDashboard: pubdash, } - pubdash, err := hs.dashboardService.SavePublicDashboardConfig(c.Req.Context(), &dto) + pubdash, err := hs.DashboardService.SavePublicDashboardConfig(c.Req.Context(), &dto) if err != nil { return handleDashboardErr(http.StatusInternalServerError, "Failed to save public dashboard configuration", err) } @@ -87,17 +87,17 @@ func (hs *HTTPServer) QueryPublicDashboard(c *models.ReqContext) response.Respon return response.Error(http.StatusBadRequest, "invalid panel ID", err) } - dashboard, err := hs.dashboardService.GetPublicDashboard(c.Req.Context(), web.Params(c.Req)[":accessToken"]) + 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) + 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( + reqDTO, err := hs.DashboardService.BuildPublicDashboardMetricRequest( c.Req.Context(), dashboard, publicDashboard, diff --git a/pkg/api/dashboard_public_test.go b/pkg/api/dashboard_public_test.go index 3c99b40b74d..9ea6360a038 100644 --- a/pkg/api/dashboard_public_test.go +++ b/pkg/api/dashboard_public_test.go @@ -38,7 +38,7 @@ func TestAPIGetPublicDashboard(t *testing.T) { dashSvc := dashboards.NewFakeDashboardService(t) dashSvc.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")). Return(&models.Dashboard{}, nil).Maybe() - sc.hs.dashboardService = dashSvc + sc.hs.DashboardService = dashSvc setInitCtxSignedInViewer(sc.initCtx) response := callAPI( @@ -97,7 +97,7 @@ func TestAPIGetPublicDashboard(t *testing.T) { dashSvc := dashboards.NewFakeDashboardService(t) dashSvc.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")). Return(test.publicDashboardResult, test.publicDashboardErr) - sc.hs.dashboardService = dashSvc + sc.hs.DashboardService = dashSvc setInitCtxSignedInViewer(sc.initCtx) response := callAPI( @@ -170,7 +170,7 @@ func TestAPIGetPublicDashboardConfig(t *testing.T) { dashSvc := dashboards.NewFakeDashboardService(t) dashSvc.On("GetPublicDashboardConfig", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")). Return(test.PublicDashboardResult, test.PublicDashboardError) - sc.hs.dashboardService = dashSvc + sc.hs.DashboardService = dashSvc setInitCtxSignedInViewer(sc.initCtx) response := callAPI( @@ -229,7 +229,7 @@ func TestApiSavePublicDashboardConfig(t *testing.T) { dashSvc := dashboards.NewFakeDashboardService(t) dashSvc.On("SavePublicDashboardConfig", mock.Anything, mock.AnythingOfType("*dashboards.SavePublicDashboardConfigDTO")). Return(&models.PublicDashboard{IsEnabled: true}, test.saveDashboardError) - sc.hs.dashboardService = dashSvc + sc.hs.DashboardService = dashSvc setInitCtxSignedInViewer(sc.initCtx) response := callAPI( @@ -298,7 +298,7 @@ func TestAPIQueryPublicDashboard(t *testing.T) { return SetupAPITestServer(t, func(hs *HTTPServer) { hs.queryDataService = qds hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards, enabled) - hs.dashboardService = fakeDashboardService + hs.DashboardService = fakeDashboardService }), fakeDashboardService } @@ -593,7 +593,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) }, } - pubdash, err := scenario.hs.dashboardService.SavePublicDashboardConfig(context.Background(), savePubDashboardCmd) + pubdash, err := scenario.hs.DashboardService.SavePublicDashboardConfig(context.Background(), savePubDashboardCmd) require.NoError(t, err) response := callAPI( diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 7ababd06b01..d43a7383faf 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -139,7 +139,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { SQLStore: mockSQLStore, AccessControl: accesscontrolmock.New(), Features: featuremgmt.WithFeatures(), - dashboardService: dashboardService, + DashboardService: dashboardService, dashboardVersionService: fakeDashboardVersionService, } hs.CoremodelStaticRegistry, hs.CoremodelRegistry = setupDashboardCoremodel(t) @@ -259,7 +259,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { LibraryElementService: &mockLibraryElementService{}, SQLStore: mockSQLStore, AccessControl: accesscontrolmock.New(), - dashboardService: dashboardService, + DashboardService: dashboardService, dashboardVersionService: fakeDashboardVersionService, } hs.CoremodelStaticRegistry, hs.CoremodelRegistry = setupDashboardCoremodel(t) @@ -900,7 +900,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { dashboardProvisioningService: mockDashboardProvisioningService{}, SQLStore: mockSQLStore, AccessControl: accesscontrolmock.New(), - dashboardService: dashboardService, + DashboardService: dashboardService, } hs.CoremodelStaticRegistry, hs.CoremodelRegistry = setupDashboardCoremodel(t) hs.callGetDashboard(sc) @@ -954,7 +954,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr cfg, dashboardStore, nil, features, folderPermissions, dashboardPermissions, ac, ), - dashboardService: dashboardService, + DashboardService: dashboardService, } hs.CoremodelStaticRegistry, hs.CoremodelRegistry = setupDashboardCoremodel(t) @@ -986,7 +986,7 @@ func (hs *HTTPServer) callGetDashboardVersions(sc *scenarioContext) { func (hs *HTTPServer) callDeleteDashboardByUID(t *testing.T, sc *scenarioContext, mockDashboard *dashboards.FakeDashboardService) { - hs.dashboardService = mockDashboard + hs.DashboardService = mockDashboard sc.handlerFunc = hs.DeleteDashboardByUID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() } @@ -1018,7 +1018,7 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s pluginStore: &fakePluginStore{}, LibraryPanelService: &mockLibraryPanelService{}, LibraryElementService: &mockLibraryElementService{}, - dashboardService: dashboardService, + DashboardService: dashboardService, folderService: folderService, Features: featuremgmt.WithFeatures(), } @@ -1088,7 +1088,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout QuotaService: "a.QuotaService{Cfg: cfg}, LibraryPanelService: &mockLibraryPanelService{}, LibraryElementService: &mockLibraryElementService{}, - dashboardService: mock, + DashboardService: mock, SQLStore: sqlStore, Features: featuremgmt.WithFeatures(), dashboardVersionService: fakeDashboardVersionService, diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index c0fe84cb2dc..343b5f6c1b2 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -70,6 +70,8 @@ type MetricRequest struct { // required: false Debug bool `json:"debug"` + PublicDashboardAccessToken string `json:"publicDashboardAccessToken"` + HTTPRequest *http.Request `json:"-"` } diff --git a/pkg/api/folder_permission.go b/pkg/api/folder_permission.go index e622c21ab05..892e9e043aa 100644 --- a/pkg/api/folder_permission.go +++ b/pkg/api/folder_permission.go @@ -126,7 +126,7 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext) response.Res return response.Success("Dashboard permissions updated") } - if err := hs.dashboardService.UpdateDashboardACL(c.Req.Context(), folder.Id, items); err != nil { + if err := hs.DashboardService.UpdateDashboardACL(c.Req.Context(), folder.Id, items); err != nil { if errors.Is(err, models.ErrDashboardAclInfoMissing) { err = models.ErrFolderAclInfoMissing } diff --git a/pkg/api/folder_permission_test.go b/pkg/api/folder_permission_test.go index 48e88febbed..093522d2d9e 100644 --- a/pkg/api/folder_permission_test.go +++ b/pkg/api/folder_permission_test.go @@ -42,7 +42,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { folderService: folderService, folderPermissionsService: folderPermissions, dashboardPermissionsService: dashboardPermissions, - dashboardService: service.ProvideDashboardService( + DashboardService: service.ProvideDashboardService( settings, dashboardStore, nil, features, folderPermissions, dashboardPermissions, ac, ), AccessControl: accesscontrolmock.New().WithDisabled(), diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index e633beffd1f..db9438ca4a2 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -148,7 +148,7 @@ type HTTPServer struct { authenticator loginpkg.Authenticator teamPermissionsService accesscontrol.TeamPermissionsService NotificationService *notifications.NotificationService - dashboardService dashboards.DashboardService + DashboardService dashboards.DashboardService dashboardProvisioningService dashboards.DashboardProvisioningService folderService dashboards.FolderService DatasourcePermissionsService permissions.DatasourcePermissionsService @@ -267,7 +267,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi authInfoService: authInfoService, authenticator: authenticator, NotificationService: notificationService, - dashboardService: dashboardService, + DashboardService: dashboardService, dashboardProvisioningService: dashboardProvisioningService, folderService: folderService, DatasourcePermissionsService: datasourcePermissionsService, diff --git a/pkg/api/index.go b/pkg/api/index.go index 5a9189151fd..a6417214748 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -407,7 +407,7 @@ func (hs *HTTPServer) buildStarredItemsNavLinks(c *models.ReqContext, prefs *pre Id: dashboardId, OrgId: c.OrgId, } - err := hs.dashboardService.GetDashboard(c.Req.Context(), query) + err := hs.DashboardService.GetDashboard(c.Req.Context(), query) if err == nil { starredDashboards = append(starredDashboards, query.Result) } @@ -674,7 +674,7 @@ func (hs *HTTPServer) buildAdminNavLinks(c *models.ReqContext) []*dtos.NavLink { func (hs *HTTPServer) editorInAnyFolder(c *models.ReqContext) bool { hasEditPermissionInFoldersQuery := models.HasEditPermissionInFoldersQuery{SignedInUser: c.SignedInUser} - if err := hs.dashboardService.HasEditPermissionInFolders(c.Req.Context(), &hasEditPermissionInFoldersQuery); err != nil { + if err := hs.DashboardService.HasEditPermissionInFolders(c.Req.Context(), &hasEditPermissionInFoldersQuery); err != nil { return false } return hasEditPermissionInFoldersQuery.Result diff --git a/pkg/api/playlist_play.go b/pkg/api/playlist_play.go index c75a550d579..035c4d402f4 100644 --- a/pkg/api/playlist_play.go +++ b/pkg/api/playlist_play.go @@ -16,7 +16,7 @@ func (hs *HTTPServer) populateDashboardsByID(ctx context.Context, dashboardByIDs if len(dashboardByIDs) > 0 { dashboardQuery := models.GetDashboardsQuery{DashboardIds: dashboardByIDs} - if err := hs.dashboardService.GetDashboards(ctx, &dashboardQuery); err != nil { + if err := hs.DashboardService.GetDashboards(ctx, &dashboardQuery); err != nil { return result, err } diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index e1acad29947..e13f6b07837 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -31,7 +31,7 @@ func (hs *HTTPServer) SetHomeDashboard(c *models.ReqContext) response.Response { dashboardID := cmd.HomeDashboardID if cmd.HomeDashboardUID != nil { query := models.GetDashboardQuery{Uid: *cmd.HomeDashboardUID} - err := hs.dashboardService.GetDashboard(c.Req.Context(), &query) + err := hs.DashboardService.GetDashboard(c.Req.Context(), &query) if err != nil { return response.Error(404, "Dashboard not found", err) } @@ -65,7 +65,7 @@ func (hs *HTTPServer) getPreferencesFor(ctx context.Context, orgID, userID, team // when homedashboardID is 0, that means it is the default home dashboard, no UID would be returned in the response if preference.HomeDashboardID != 0 { query := models.GetDashboardQuery{Id: preference.HomeDashboardID, OrgId: orgID} - err = hs.dashboardService.GetDashboard(ctx, &query) + err = hs.DashboardService.GetDashboard(ctx, &query) if err == nil { dashboardUID = query.Result.Uid } @@ -105,7 +105,7 @@ func (hs *HTTPServer) updatePreferencesFor(ctx context.Context, orgID, userID, t dashboardID := dtoCmd.HomeDashboardID if dtoCmd.HomeDashboardUID != nil { query := models.GetDashboardQuery{Uid: *dtoCmd.HomeDashboardUID, OrgId: orgID} - err := hs.dashboardService.GetDashboard(ctx, &query) + err := hs.DashboardService.GetDashboard(ctx, &query) if err != nil { return response.Error(404, "Dashboard not found", err) } @@ -151,7 +151,7 @@ func (hs *HTTPServer) patchPreferencesFor(ctx context.Context, orgID, userID, te dashboardID := dtoCmd.HomeDashboardID if dtoCmd.HomeDashboardUID != nil { query := models.GetDashboardQuery{Uid: *dtoCmd.HomeDashboardUID, OrgId: orgID} - err := hs.dashboardService.GetDashboard(ctx, &query) + err := hs.DashboardService.GetDashboard(ctx, &query) if err != nil { return response.Error(404, "Dashboard not found", err) } diff --git a/pkg/api/preferences_test.go b/pkg/api/preferences_test.go index e62af23b504..5796ad1e867 100644 --- a/pkg/api/preferences_test.go +++ b/pkg/api/preferences_test.go @@ -40,7 +40,7 @@ func TestAPIEndpoint_GetCurrentOrgPreferences_LegacyAccessControl(t *testing.T) q.Result = &models.Dashboard{Uid: "home", Id: 1} }).Return(nil) - sc.hs.dashboardService = dashSvc + sc.hs.DashboardService = dashSvc prefService := preftest.NewPreferenceServiceFake() prefService.ExpectedPreference = &pref.Preference{HomeDashboardID: 1, Theme: "dark"} @@ -169,7 +169,7 @@ func TestAPIEndpoint_PatchUserPreferences(t *testing.T) { q := args.Get(1).(*models.GetDashboardQuery) q.Result = &models.Dashboard{Uid: "home", Id: 1} }).Return(nil) - sc.hs.dashboardService = dashSvc + sc.hs.DashboardService = dashSvc t.Run("Returns 200 on success", func(t *testing.T) { response := callAPI(sc.server, http.MethodPatch, patchUserPreferencesUrl, input, t) assert.Equal(t, http.StatusOK, response.Code) diff --git a/pkg/api/stars.go b/pkg/api/stars.go index 73eeae19ab1..fb8e76114ac 100644 --- a/pkg/api/stars.go +++ b/pkg/api/stars.go @@ -26,7 +26,7 @@ func (hs *HTTPServer) GetStars(c *models.ReqContext) response.Response { Id: dashboardId, OrgId: c.OrgId, } - err := hs.dashboardService.GetDashboard(c.Req.Context(), query) + err := hs.DashboardService.GetDashboard(c.Req.Context(), query) // Grafana admin users may have starred dashboards in multiple orgs. This will avoid returning errors when the dashboard is in another org if err == nil { diff --git a/pkg/models/dashboard_queries.go b/pkg/models/dashboard_queries.go index 0490071b116..7f27011e73c 100644 --- a/pkg/models/dashboard_queries.go +++ b/pkg/models/dashboard_queries.go @@ -4,7 +4,23 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" ) -func GetQueriesFromDashboard(dashboard *simplejson.Json) map[int64][]*simplejson.Json { +func GetUniqueDashboardDatasourceUids(dashboard *simplejson.Json) []string { + var datasourceUids []string + exists := map[string]bool{} + + for _, panelObj := range dashboard.Get("panels").MustArray() { + panel := simplejson.NewFromAny(panelObj) + uid := panel.Get("datasource").Get("uid").MustString() + if _, ok := exists[uid]; !ok { + datasourceUids = append(datasourceUids, uid) + exists[uid] = true + } + } + + return datasourceUids +} + +func GroupQueriesByPanelId(dashboard *simplejson.Json) map[int64][]*simplejson.Json { result := make(map[int64][]*simplejson.Json) for _, panelObj := range dashboard.Get("panels").MustArray() { diff --git a/pkg/models/dashboard_queries_test.go b/pkg/models/dashboard_queries_test.go index 95daa3bae22..ed02d11c88b 100644 --- a/pkg/models/dashboard_queries_test.go +++ b/pkg/models/dashboard_queries_test.go @@ -56,6 +56,79 @@ const ( "schemaVersion": 35 }` + dashboardWithDuplicateDatasources = ` +{ + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "abc123" + }, + "id": 1, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "abc123" + }, + "exemplar": true, + "expr": "go_goroutines{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Panel Title", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "_yxMP8Ynk" + }, + "id": 2, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "_yxMP8Ynk" + }, + "exemplar": true, + "expr": "go_goroutines{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Panel Title", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "_yxMP8Ynk" + }, + "id": 3, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "_yxMP8Ynk" + }, + "exemplar": true, + "expr": "go_goroutines{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Panel Title", + "type": "timeseries" + } + ], + "schemaVersion": 35 +}` + oldStyleDashboard = ` { "panels": [ @@ -79,12 +152,32 @@ const ( }` ) -func TestGetQueriesFromDashboard(t *testing.T) { +func TestGetUniqueDashboardDatasourceUids(t *testing.T) { + t.Run("can get unique datasource ids from dashboard", func(t *testing.T) { + json, err := simplejson.NewJson([]byte(dashboardWithDuplicateDatasources)) + require.NoError(t, err) + + uids := GetUniqueDashboardDatasourceUids(json) + require.Len(t, uids, 2) + require.Equal(t, "abc123", uids[0]) + require.Equal(t, "_yxMP8Ynk", uids[1]) + }) + + t.Run("can get no datasource uids from empty dashboard", func(t *testing.T) { + json, err := simplejson.NewJson([]byte(`{"panels": {}}`)) + require.NoError(t, err) + + uids := GetUniqueDashboardDatasourceUids(json) + require.Len(t, uids, 0) + }) +} + +func TestGroupQueriesByPanelId(t *testing.T) { t.Run("can extract no queries from empty dashboard", func(t *testing.T) { json, err := simplejson.NewJson([]byte(`{"panels": {}}`)) require.NoError(t, err) - queries := GetQueriesFromDashboard(json) + queries := GroupQueriesByPanelId(json) require.Len(t, queries, 0) }) @@ -92,7 +185,7 @@ func TestGetQueriesFromDashboard(t *testing.T) { json, err := simplejson.NewJson([]byte(dashboardWithNoQueries)) require.NoError(t, err) - queries := GetQueriesFromDashboard(json) + queries := GroupQueriesByPanelId(json) require.Len(t, queries, 1) require.Contains(t, queries, int64(2)) require.Len(t, queries[2], 0) @@ -102,7 +195,7 @@ func TestGetQueriesFromDashboard(t *testing.T) { json, err := simplejson.NewJson([]byte(dashboardWithQueries)) require.NoError(t, err) - queries := GetQueriesFromDashboard(json) + queries := GroupQueriesByPanelId(json) require.Len(t, queries, 1) require.Contains(t, queries, int64(2)) require.Len(t, queries[2], 2) @@ -138,7 +231,7 @@ func TestGetQueriesFromDashboard(t *testing.T) { json, err := simplejson.NewJson([]byte(oldStyleDashboard)) require.NoError(t, err) - queries := GetQueriesFromDashboard(json) + queries := GroupQueriesByPanelId(json) require.Len(t, queries, 1) require.Contains(t, queries, int64(2)) require.Len(t, queries[2], 1) diff --git a/pkg/services/dashboards/dashboard.go b/pkg/services/dashboards/dashboard.go index c47af49680d..c3cbb277e00 100644 --- a/pkg/services/dashboards/dashboard.go +++ b/pkg/services/dashboards/dashboard.go @@ -11,6 +11,7 @@ import ( // 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) diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go index 7f46f43096c..0c97c70ad24 100644 --- a/pkg/services/dashboards/dashboard_service_mock.go +++ b/pkg/services/dashboards/dashboard_service_mock.go @@ -18,6 +18,29 @@ 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) diff --git a/pkg/services/dashboards/service/dashboard_public.go b/pkg/services/dashboards/service/dashboard_public.go index 3da5b9e2f31..873c611f538 100644 --- a/pkg/services/dashboards/service/dashboard_public.go +++ b/pkg/services/dashboards/service/dashboard_public.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/datasources" ) // Gets public dashboard via access token @@ -124,7 +125,7 @@ func (dr *DashboardServiceImpl) BuildPublicDashboardMetricRequest(ctx context.Co return dtos.MetricRequest{}, dashboards.ErrPublicDashboardNotFound } - queriesByPanel := models.GetQueriesFromDashboard(dashboard.Data) + queriesByPanel := models.GroupQueriesByPanelId(dashboard.Data) if _, ok := queriesByPanel[panelId]; !ok { return dtos.MetricRequest{}, dashboards.ErrPublicDashboardPanelNotFound @@ -139,6 +140,26 @@ func (dr *DashboardServiceImpl) BuildPublicDashboardMetricRequest(ctx context.Co }, nil } +// 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) { + datasourceUids := models.GetUniqueDashboardDatasourceUids(dashboard.Data) + + // 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) + queryScopes := make([]string, 0) + readScopes := make([]string, 0) + for _, uid := range datasourceUids { + queryScopes = append(queryScopes, fmt.Sprintf("datasources:uid:%s", uid)) + readScopes = append(readScopes, fmt.Sprintf("datasources:uid:%s", uid)) + } + permissions[datasources.ActionQuery] = queryScopes + permissions[datasources.ActionRead] = readScopes + anonymousUser.Permissions[dashboard.OrgId] = permissions + + return anonymousUser, nil +} + // generates a uuid formatted without dashes to use as access token func GenerateAccessToken() (string, error) { token, err := uuid.NewRandom() diff --git a/pkg/services/dashboards/service/dashboard_public_test.go b/pkg/services/dashboards/service/dashboard_public_test.go index 74e2efd62f3..8da35c5c202 100644 --- a/pkg/services/dashboards/service/dashboard_public_test.go +++ b/pkg/services/dashboards/service/dashboard_public_test.go @@ -314,6 +314,26 @@ func TestUpdatePublicDashboard(t *testing.T) { }) } +func TestBuildAnonymousUser(t *testing.T) { + sqlStore := sqlstore.InitTestDB(t) + dashboardStore := database.ProvideDashboardStore(sqlStore) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) + service := &DashboardServiceImpl{ + log: log.New("test.logger"), + dashboardStore: dashboardStore, + } + + t.Run("will add datasource read and query permissions to user for each datasource in dashboard", func(t *testing.T) { + user, err := service.BuildAnonymousUser(context.Background(), dashboard) + require.NoError(t, err) + require.Equal(t, dashboard.OrgId, user.OrgId) + require.Equal(t, "datasources:uid:ds1", user.Permissions[user.OrgId]["datasources:query"][0]) + require.Equal(t, "datasources:uid:ds3", user.Permissions[user.OrgId]["datasources:query"][1]) + require.Equal(t, "datasources:uid:ds1", user.Permissions[user.OrgId]["datasources:read"][0]) + require.Equal(t, "datasources:uid:ds3", user.Permissions[user.OrgId]["datasources:read"][1]) + }) +} + func TestBuildPublicDashboardMetricRequest(t *testing.T) { sqlStore := sqlstore.InitTestDB(t) dashboardStore := database.ProvideDashboardStore(sqlStore) @@ -425,6 +445,9 @@ func insertTestDashboard(t *testing.T, dashboardStore *database.DashboardStore, "panels": []interface{}{ map[string]interface{}{ "id": 1, + "datasource": map[string]interface{}{ + "uid": "ds1", + }, "targets": []interface{}{ map[string]interface{}{ "datasource": map[string]interface{}{ @@ -444,6 +467,9 @@ func insertTestDashboard(t *testing.T, dashboardStore *database.DashboardStore, }, map[string]interface{}{ "id": 2, + "datasource": map[string]interface{}{ + "uid": "ds3", + }, "targets": []interface{}{ map[string]interface{}{ "datasource": map[string]interface{}{ diff --git a/public/app/features/dashboard/services/PublicDashboardDataSource.ts b/public/app/features/dashboard/services/PublicDashboardDataSource.ts index 7a384b0835d..52e89d55fba 100644 --- a/public/app/features/dashboard/services/PublicDashboardDataSource.ts +++ b/public/app/features/dashboard/services/PublicDashboardDataSource.ts @@ -1,19 +1,41 @@ import { catchError, Observable, of, switchMap } from 'rxjs'; -import { DataQuery, DataQueryRequest, DataQueryResponse, DataSourceApi, PluginMeta } from '@grafana/data'; +import { + DataQuery, + DataQueryRequest, + DataQueryResponse, + DataSourceApi, + DataSourceRef, + PluginMeta, +} from '@grafana/data'; import { BackendDataSourceResponse, getBackendSrv, toDataQueryResponse } from '@grafana/runtime'; +export const PUBLIC_DATASOURCE = '-- Public --'; + export class PublicDashboardDataSource extends DataSourceApi { - constructor() { + constructor(datasource: DataSourceRef | string | DataSourceApi | null) { super({ name: 'public-ds', - id: 1, + id: 0, type: 'public-ds', meta: {} as PluginMeta, - uid: '1', + uid: PublicDashboardDataSource.resolveUid(datasource), jsonData: {}, access: 'proxy', }); + + this.interval = '1min'; + } + + /** + * Get the datasource uid based on the many types a datasource can be. + */ + private static resolveUid(datasource: DataSourceRef | string | DataSourceApi | null): string { + if (typeof datasource === 'string') { + return datasource; + } + + return datasource?.uid ?? PUBLIC_DATASOURCE; } /** diff --git a/public/app/features/query/state/PanelQueryRunner.ts b/public/app/features/query/state/PanelQueryRunner.ts index a298229d44e..931954f87f8 100644 --- a/public/app/features/query/state/PanelQueryRunner.ts +++ b/public/app/features/query/state/PanelQueryRunner.ts @@ -362,7 +362,7 @@ async function getDataSource( publicDashboardAccessToken?: string ): Promise { if (publicDashboardAccessToken) { - return new PublicDashboardDataSource(); + return new PublicDashboardDataSource(datasource); } if (datasource && (datasource as any).query) {