From 5b9959014c5175a0ec4cfde6162e22bde568225a Mon Sep 17 00:00:00 2001 From: Ezequiel Victorero Date: Fri, 21 Oct 2022 09:37:38 -0300 Subject: [PATCH] PublicDashboards: refactor service (#57372) --- .../publicdashboards/api/middleware.go | 2 +- .../publicdashboards/api/middleware_test.go | 2 +- .../publicdashboards/database/database.go | 9 +- .../database/database_test.go | 26 +- .../public_dashboard_service_mock.go | 92 +- .../public_dashboard_store_mock.go | 6 +- .../publicdashboards/publicdashboard.go | 7 +- .../queries/dashboard_queries.go | 102 -- .../queries/dashboard_queries_test.go | 678 --------- .../publicdashboards/service/query.go | 288 ++++ .../publicdashboards/service/query_test.go | 1252 +++++++++++++++++ .../publicdashboards/service/service.go | 194 +-- .../publicdashboards/service/service_test.go | 626 --------- 13 files changed, 1605 insertions(+), 1679 deletions(-) delete mode 100644 pkg/services/publicdashboards/queries/dashboard_queries.go delete mode 100644 pkg/services/publicdashboards/queries/dashboard_queries_test.go create mode 100644 pkg/services/publicdashboards/service/query.go create mode 100644 pkg/services/publicdashboards/service/query_test.go diff --git a/pkg/services/publicdashboards/api/middleware.go b/pkg/services/publicdashboards/api/middleware.go index 0b17ff9ee59..545ef718a38 100644 --- a/pkg/services/publicdashboards/api/middleware.go +++ b/pkg/services/publicdashboards/api/middleware.go @@ -50,7 +50,7 @@ func RequiresExistingAccessToken(publicDashboardService publicdashboards.Service } // Check that the access token references an enabled public dashboard - exists, err := publicDashboardService.AccessTokenExists(c.Req.Context(), accessToken) + exists, err := publicDashboardService.PublicDashboardEnabledExistsByAccessToken(c.Req.Context(), accessToken) if err != nil { c.JsonApiErr(http.StatusInternalServerError, "Failed to query access token", nil) return diff --git a/pkg/services/publicdashboards/api/middleware_test.go b/pkg/services/publicdashboards/api/middleware_test.go index 4916706829f..1c380e4a896 100644 --- a/pkg/services/publicdashboards/api/middleware_test.go +++ b/pkg/services/publicdashboards/api/middleware_test.go @@ -74,7 +74,7 @@ func TestRequiresExistingAccessToken(t *testing.T) { for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { publicdashboardService := &publicdashboards.FakePublicDashboardService{} - publicdashboardService.On("AccessTokenExists", mock.Anything, mock.Anything).Return(tt.AccessTokenExists, tt.AccessTokenExistsErr) + publicdashboardService.On("PublicDashboardEnabledExistsByAccessToken", mock.Anything, mock.Anything).Return(tt.AccessTokenExists, tt.AccessTokenExistsErr) params := map[string]string{":accessToken": tt.AccessToken} mw := RequiresExistingAccessToken(publicdashboardService) _, resp := runMw(t, nil, "GET", tt.Path, params, mw) diff --git a/pkg/services/publicdashboards/database/database.go b/pkg/services/publicdashboards/database/database.go index d80fca41457..f6ef44a7009 100644 --- a/pkg/services/publicdashboards/database/database.go +++ b/pkg/services/publicdashboards/database/database.go @@ -258,8 +258,8 @@ func (d *PublicDashboardStoreImpl) UpdatePublicDashboardConfig(ctx context.Conte return err } -// Responds true if public dashboard for a dashboard exists and isEnabled -func (d *PublicDashboardStoreImpl) PublicDashboardEnabled(ctx context.Context, dashboardUid string) (bool, error) { +// EnabledPublicDashboardExistsByDashboardUid Responds true if there is an enabled public dashboard for a dashboard uid +func (d *PublicDashboardStoreImpl) PublicDashboardEnabledExistsByDashboardUid(ctx context.Context, dashboardUid string) (bool, error) { hasPublicDashboard := false err := d.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error { sql := "SELECT COUNT(*) FROM dashboard_public WHERE dashboard_uid=? AND is_enabled=true" @@ -276,9 +276,8 @@ func (d *PublicDashboardStoreImpl) PublicDashboardEnabled(ctx context.Context, d return hasPublicDashboard, err } -// Responds true if accessToken exists and isEnabled. May be renamed in the -// future -func (d *PublicDashboardStoreImpl) AccessTokenExists(ctx context.Context, accessToken string) (bool, error) { +// EnabledPublicDashboardExistsByAccessToken Responds true if accessToken exists and isEnabled +func (d *PublicDashboardStoreImpl) PublicDashboardEnabledExistsByAccessToken(ctx context.Context, accessToken string) (bool, error) { hasPublicDashboard := false err := d.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error { sql := "SELECT COUNT(*) FROM dashboard_public WHERE access_token=? AND is_enabled=true" diff --git a/pkg/services/publicdashboards/database/database_test.go b/pkg/services/publicdashboards/database/database_test.go index 2ba97829866..17d8590551a 100644 --- a/pkg/services/publicdashboards/database/database_test.go +++ b/pkg/services/publicdashboards/database/database_test.go @@ -88,8 +88,7 @@ func TestIntegrationGetDashboard(t *testing.T) { }) } -// AccessTokenExists -func TestIntegrationAccessTokenExists(t *testing.T) { +func TestIntegrationEnabledPublicDashboardExistsByAccessToken(t *testing.T) { var sqlStore db.DB var cfg *setting.Cfg var dashboardStore *dashboardsDB.DashboardStore @@ -102,7 +101,7 @@ func TestIntegrationAccessTokenExists(t *testing.T) { publicdashboardStore = ProvideStore(sqlStore) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) } - t.Run("AccessTokenExists will return true when at least one public dashboard has a matching access token", func(t *testing.T) { + t.Run("PublicDashboardEnabledExistsByAccessToken will return true when at least one public dashboard has a matching access token", func(t *testing.T) { setup() err := publicdashboardStore.SavePublicDashboardConfig(context.Background(), SavePublicDashboardConfigCommand{ @@ -118,13 +117,13 @@ func TestIntegrationAccessTokenExists(t *testing.T) { }) require.NoError(t, err) - res, err := publicdashboardStore.AccessTokenExists(context.Background(), "accessToken") + res, err := publicdashboardStore.PublicDashboardEnabledExistsByAccessToken(context.Background(), "accessToken") require.NoError(t, err) require.True(t, res) }) - t.Run("AccessTokenExists will return false when IsEnabled=false", func(t *testing.T) { + t.Run("PublicDashboardEnabledExistsByAccessToken will return false when IsEnabled=false", func(t *testing.T) { setup() err := publicdashboardStore.SavePublicDashboardConfig(context.Background(), SavePublicDashboardConfigCommand{ @@ -140,24 +139,23 @@ func TestIntegrationAccessTokenExists(t *testing.T) { }) require.NoError(t, err) - res, err := publicdashboardStore.AccessTokenExists(context.Background(), "accessToken") + res, err := publicdashboardStore.PublicDashboardEnabledExistsByAccessToken(context.Background(), "accessToken") require.NoError(t, err) require.False(t, res) }) - t.Run("AccessTokenExists will return false when no public dashboard has matching access token", func(t *testing.T) { + t.Run("PublicDashboardEnabledExistsByAccessToken will return false when no public dashboard has matching access token", func(t *testing.T) { setup() - res, err := publicdashboardStore.AccessTokenExists(context.Background(), "accessToken") + res, err := publicdashboardStore.PublicDashboardEnabledExistsByAccessToken(context.Background(), "accessToken") require.NoError(t, err) require.False(t, res) }) } -// PublicDashboardEnabled -func TestIntegrationPublicDashboardEnabled(t *testing.T) { +func TestIntegrationEnabledPublicDashboardExistsByDashboardUid(t *testing.T) { var sqlStore db.DB var cfg *setting.Cfg var dashboardStore *dashboardsDB.DashboardStore @@ -171,7 +169,7 @@ func TestIntegrationPublicDashboardEnabled(t *testing.T) { savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) } - t.Run("PublicDashboardEnabled Will return true when dashboard has at least one enabled public dashboard", func(t *testing.T) { + t.Run("PublicDashboardEnabledExistsByDashboardUid Will return true when dashboard has at least one enabled public dashboard", func(t *testing.T) { setup() err := publicdashboardStore.SavePublicDashboardConfig(context.Background(), SavePublicDashboardConfigCommand{ @@ -187,13 +185,13 @@ func TestIntegrationPublicDashboardEnabled(t *testing.T) { }) require.NoError(t, err) - res, err := publicdashboardStore.PublicDashboardEnabled(context.Background(), savedDashboard.Uid) + res, err := publicdashboardStore.PublicDashboardEnabledExistsByDashboardUid(context.Background(), savedDashboard.Uid) require.NoError(t, err) require.True(t, res) }) - t.Run("PublicDashboardEnabled will return false when dashboard has public dashboards but they are not enabled", func(t *testing.T) { + t.Run("PublicDashboardEnabledExistsByDashboardUid will return false when dashboard has public dashboards but they are not enabled", func(t *testing.T) { setup() err := publicdashboardStore.SavePublicDashboardConfig(context.Background(), SavePublicDashboardConfigCommand{ @@ -209,7 +207,7 @@ func TestIntegrationPublicDashboardEnabled(t *testing.T) { }) require.NoError(t, err) - res, err := publicdashboardStore.PublicDashboardEnabled(context.Background(), savedDashboard.Uid) + res, err := publicdashboardStore.PublicDashboardEnabledExistsByDashboardUid(context.Background(), savedDashboard.Uid) require.NoError(t, err) require.False(t, res) diff --git a/pkg/services/publicdashboards/public_dashboard_service_mock.go b/pkg/services/publicdashboards/public_dashboard_service_mock.go index 95b9cca033e..9cc5e16b3c7 100644 --- a/pkg/services/publicdashboards/public_dashboard_service_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_service_mock.go @@ -11,9 +11,9 @@ import ( mock "github.com/stretchr/testify/mock" - models "github.com/grafana/grafana/pkg/models" + models "github.com/grafana/grafana/pkg/services/publicdashboards/models" - publicdashboardsmodels "github.com/grafana/grafana/pkg/services/publicdashboards/models" + pkgmodels "github.com/grafana/grafana/pkg/models" user "github.com/grafana/grafana/pkg/services/user" ) @@ -24,7 +24,7 @@ type FakePublicDashboardService struct { } // AccessTokenExists provides a mock function with given fields: ctx, accessToken -func (_m *FakePublicDashboardService) AccessTokenExists(ctx context.Context, accessToken string) (bool, error) { +func (_m *FakePublicDashboardService) PublicDashboardEnabledExistsByAccessToken(ctx context.Context, accessToken string) (bool, error) { ret := _m.Called(ctx, accessToken) var r0 bool @@ -44,37 +44,21 @@ func (_m *FakePublicDashboardService) AccessTokenExists(ctx context.Context, acc return r0, r1 } -// BuildAnonymousUser provides a mock function with given fields: ctx, dashboard -func (_m *FakePublicDashboardService) BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) *user.SignedInUser { - ret := _m.Called(ctx, dashboard) - - var r0 *user.SignedInUser - if rf, ok := ret.Get(0).(func(context.Context, *models.Dashboard) *user.SignedInUser); ok { - r0 = rf(ctx, dashboard) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*user.SignedInUser) - } - } - - return r0 -} - // GetAnnotations provides a mock function with given fields: ctx, reqDTO, accessToken -func (_m *FakePublicDashboardService) GetAnnotations(ctx context.Context, reqDTO publicdashboardsmodels.AnnotationsQueryDTO, accessToken string) ([]publicdashboardsmodels.AnnotationEvent, error) { +func (_m *FakePublicDashboardService) GetAnnotations(ctx context.Context, reqDTO models.AnnotationsQueryDTO, accessToken string) ([]models.AnnotationEvent, error) { ret := _m.Called(ctx, reqDTO, accessToken) - var r0 []publicdashboardsmodels.AnnotationEvent - if rf, ok := ret.Get(0).(func(context.Context, publicdashboardsmodels.AnnotationsQueryDTO, string) []publicdashboardsmodels.AnnotationEvent); ok { + var r0 []models.AnnotationEvent + if rf, ok := ret.Get(0).(func(context.Context, models.AnnotationsQueryDTO, string) []models.AnnotationEvent); ok { r0 = rf(ctx, reqDTO, accessToken) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]publicdashboardsmodels.AnnotationEvent) + r0 = ret.Get(0).([]models.AnnotationEvent) } } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, publicdashboardsmodels.AnnotationsQueryDTO, string) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, models.AnnotationsQueryDTO, string) error); ok { r1 = rf(ctx, reqDTO, accessToken) } else { r1 = ret.Error(1) @@ -84,15 +68,15 @@ func (_m *FakePublicDashboardService) GetAnnotations(ctx context.Context, reqDTO } // GetDashboard provides a mock function with given fields: ctx, dashboardUid -func (_m *FakePublicDashboardService) GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) { +func (_m *FakePublicDashboardService) GetDashboard(ctx context.Context, dashboardUid string) (*pkgmodels.Dashboard, error) { ret := _m.Called(ctx, dashboardUid) - var r0 *models.Dashboard - if rf, ok := ret.Get(0).(func(context.Context, string) *models.Dashboard); ok { + var r0 *pkgmodels.Dashboard + if rf, ok := ret.Get(0).(func(context.Context, string) *pkgmodels.Dashboard); ok { r0 = rf(ctx, dashboardUid) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.Dashboard) + r0 = ret.Get(0).(*pkgmodels.Dashboard) } } @@ -107,18 +91,18 @@ func (_m *FakePublicDashboardService) GetDashboard(ctx context.Context, dashboar } // GetMetricRequest provides a mock function with given fields: ctx, dashboard, publicDashboard, panelId, reqDTO -func (_m *FakePublicDashboardService) GetMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *publicdashboardsmodels.PublicDashboard, panelId int64, reqDTO publicdashboardsmodels.PublicDashboardQueryDTO) (dtos.MetricRequest, error) { +func (_m *FakePublicDashboardService) GetMetricRequest(ctx context.Context, dashboard *pkgmodels.Dashboard, publicDashboard *models.PublicDashboard, panelId int64, reqDTO models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) { ret := _m.Called(ctx, dashboard, publicDashboard, panelId, reqDTO) var r0 dtos.MetricRequest - if rf, ok := ret.Get(0).(func(context.Context, *models.Dashboard, *publicdashboardsmodels.PublicDashboard, int64, publicdashboardsmodels.PublicDashboardQueryDTO) dtos.MetricRequest); ok { + if rf, ok := ret.Get(0).(func(context.Context, *pkgmodels.Dashboard, *models.PublicDashboard, int64, models.PublicDashboardQueryDTO) dtos.MetricRequest); ok { r0 = rf(ctx, dashboard, publicDashboard, panelId, reqDTO) } else { r0 = ret.Get(0).(dtos.MetricRequest) } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *models.Dashboard, *publicdashboardsmodels.PublicDashboard, int64, publicdashboardsmodels.PublicDashboardQueryDTO) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *pkgmodels.Dashboard, *models.PublicDashboard, int64, models.PublicDashboardQueryDTO) error); ok { r1 = rf(ctx, dashboard, publicDashboard, panelId, reqDTO) } else { r1 = ret.Error(1) @@ -128,24 +112,24 @@ func (_m *FakePublicDashboardService) GetMetricRequest(ctx context.Context, dash } // GetPublicDashboard provides a mock function with given fields: ctx, accessToken -func (_m *FakePublicDashboardService) GetPublicDashboard(ctx context.Context, accessToken string) (*publicdashboardsmodels.PublicDashboard, *models.Dashboard, error) { +func (_m *FakePublicDashboardService) GetPublicDashboard(ctx context.Context, accessToken string) (*models.PublicDashboard, *pkgmodels.Dashboard, error) { ret := _m.Called(ctx, accessToken) - var r0 *publicdashboardsmodels.PublicDashboard - if rf, ok := ret.Get(0).(func(context.Context, string) *publicdashboardsmodels.PublicDashboard); ok { + 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).(*publicdashboardsmodels.PublicDashboard) + r0 = ret.Get(0).(*models.PublicDashboard) } } - var r1 *models.Dashboard - if rf, ok := ret.Get(1).(func(context.Context, string) *models.Dashboard); ok { + 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).(*models.Dashboard) + r1 = ret.Get(1).(*pkgmodels.Dashboard) } } @@ -160,15 +144,15 @@ func (_m *FakePublicDashboardService) GetPublicDashboard(ctx context.Context, ac } // 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) { +func (_m *FakePublicDashboardService) GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.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 { + 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).(*publicdashboardsmodels.PublicDashboard) + r0 = ret.Get(0).(*models.PublicDashboard) } } @@ -204,11 +188,11 @@ func (_m *FakePublicDashboardService) GetPublicDashboardOrgId(ctx context.Contex } // GetQueryDataResponse provides a mock function with given fields: ctx, skipCache, reqDTO, panelId, accessToken -func (_m *FakePublicDashboardService) GetQueryDataResponse(ctx context.Context, skipCache bool, reqDTO publicdashboardsmodels.PublicDashboardQueryDTO, panelId int64, accessToken string) (*backend.QueryDataResponse, error) { +func (_m *FakePublicDashboardService) GetQueryDataResponse(ctx context.Context, skipCache bool, reqDTO models.PublicDashboardQueryDTO, panelId int64, accessToken string) (*backend.QueryDataResponse, error) { ret := _m.Called(ctx, skipCache, reqDTO, panelId, accessToken) var r0 *backend.QueryDataResponse - if rf, ok := ret.Get(0).(func(context.Context, bool, publicdashboardsmodels.PublicDashboardQueryDTO, int64, string) *backend.QueryDataResponse); ok { + if rf, ok := ret.Get(0).(func(context.Context, bool, models.PublicDashboardQueryDTO, int64, string) *backend.QueryDataResponse); ok { r0 = rf(ctx, skipCache, reqDTO, panelId, accessToken) } else { if ret.Get(0) != nil { @@ -217,7 +201,7 @@ func (_m *FakePublicDashboardService) GetQueryDataResponse(ctx context.Context, } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, bool, publicdashboardsmodels.PublicDashboardQueryDTO, int64, string) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, bool, models.PublicDashboardQueryDTO, int64, string) error); ok { r1 = rf(ctx, skipCache, reqDTO, panelId, accessToken) } else { r1 = ret.Error(1) @@ -227,15 +211,15 @@ func (_m *FakePublicDashboardService) GetQueryDataResponse(ctx context.Context, } // ListPublicDashboards provides a mock function with given fields: ctx, u, orgId -func (_m *FakePublicDashboardService) ListPublicDashboards(ctx context.Context, u *user.SignedInUser, orgId int64) ([]publicdashboardsmodels.PublicDashboardListResponse, error) { +func (_m *FakePublicDashboardService) ListPublicDashboards(ctx context.Context, u *user.SignedInUser, orgId int64) ([]models.PublicDashboardListResponse, error) { ret := _m.Called(ctx, u, orgId) - var r0 []publicdashboardsmodels.PublicDashboardListResponse - if rf, ok := ret.Get(0).(func(context.Context, *user.SignedInUser, int64) []publicdashboardsmodels.PublicDashboardListResponse); ok { + var r0 []models.PublicDashboardListResponse + if rf, ok := ret.Get(0).(func(context.Context, *user.SignedInUser, int64) []models.PublicDashboardListResponse); ok { r0 = rf(ctx, u, orgId) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]publicdashboardsmodels.PublicDashboardListResponse) + r0 = ret.Get(0).([]models.PublicDashboardListResponse) } } @@ -271,20 +255,20 @@ func (_m *FakePublicDashboardService) PublicDashboardEnabled(ctx context.Context } // SavePublicDashboardConfig provides a mock function with given fields: ctx, u, dto -func (_m *FakePublicDashboardService) SavePublicDashboardConfig(ctx context.Context, u *user.SignedInUser, dto *publicdashboardsmodels.SavePublicDashboardConfigDTO) (*publicdashboardsmodels.PublicDashboard, error) { +func (_m *FakePublicDashboardService) SavePublicDashboardConfig(ctx context.Context, u *user.SignedInUser, dto *models.SavePublicDashboardConfigDTO) (*models.PublicDashboard, error) { ret := _m.Called(ctx, u, dto) - var r0 *publicdashboardsmodels.PublicDashboard - if rf, ok := ret.Get(0).(func(context.Context, *user.SignedInUser, *publicdashboardsmodels.SavePublicDashboardConfigDTO) *publicdashboardsmodels.PublicDashboard); ok { + var r0 *models.PublicDashboard + if rf, ok := ret.Get(0).(func(context.Context, *user.SignedInUser, *models.SavePublicDashboardConfigDTO) *models.PublicDashboard); ok { r0 = rf(ctx, u, dto) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*publicdashboardsmodels.PublicDashboard) + r0 = ret.Get(0).(*models.PublicDashboard) } } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *user.SignedInUser, *publicdashboardsmodels.SavePublicDashboardConfigDTO) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *user.SignedInUser, *models.SavePublicDashboardConfigDTO) error); ok { r1 = rf(ctx, u, dto) } else { r1 = ret.Error(1) diff --git a/pkg/services/publicdashboards/public_dashboard_store_mock.go b/pkg/services/publicdashboards/public_dashboard_store_mock.go index d8b71e38851..5d8920ad759 100644 --- a/pkg/services/publicdashboards/public_dashboard_store_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_store_mock.go @@ -18,8 +18,8 @@ type FakePublicDashboardStore struct { mock.Mock } -// AccessTokenExists provides a mock function with given fields: ctx, accessToken -func (_m *FakePublicDashboardStore) AccessTokenExists(ctx context.Context, accessToken string) (bool, error) { +// EnabledPublicDashboardExistsByAccessToken provides a mock function with given fields: ctx, accessToken +func (_m *FakePublicDashboardStore) PublicDashboardEnabledExistsByAccessToken(ctx context.Context, accessToken string) (bool, error) { ret := _m.Called(ctx, accessToken) var r0 bool @@ -227,7 +227,7 @@ func (_m *FakePublicDashboardStore) ListPublicDashboards(ctx context.Context, or } // PublicDashboardEnabled provides a mock function with given fields: ctx, dashboardUid -func (_m *FakePublicDashboardStore) PublicDashboardEnabled(ctx context.Context, dashboardUid string) (bool, error) { +func (_m *FakePublicDashboardStore) PublicDashboardEnabledExistsByDashboardUid(ctx context.Context, dashboardUid string) (bool, error) { ret := _m.Called(ctx, dashboardUid) var r0 bool diff --git a/pkg/services/publicdashboards/publicdashboard.go b/pkg/services/publicdashboards/publicdashboard.go index e2780006a59..16831d10f07 100644 --- a/pkg/services/publicdashboards/publicdashboard.go +++ b/pkg/services/publicdashboards/publicdashboard.go @@ -14,8 +14,7 @@ import ( //go:generate mockery --name Service --structname FakePublicDashboardService --inpackage --filename public_dashboard_service_mock.go type Service interface { - AccessTokenExists(ctx context.Context, accessToken string) (bool, error) - BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) *user.SignedInUser + PublicDashboardEnabledExistsByAccessToken(ctx context.Context, accessToken string) (bool, error) GetAnnotations(ctx context.Context, reqDTO AnnotationsQueryDTO, accessToken string) ([]AnnotationEvent, error) GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) GetMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64, reqDTO PublicDashboardQueryDTO) (dtos.MetricRequest, error) @@ -30,7 +29,8 @@ type Service interface { //go:generate mockery --name Store --structname FakePublicDashboardStore --inpackage --filename public_dashboard_store_mock.go type Store interface { - AccessTokenExists(ctx context.Context, accessToken string) (bool, error) + PublicDashboardEnabledExistsByAccessToken(ctx context.Context, accessToken string) (bool, error) + PublicDashboardEnabledExistsByDashboardUid(ctx context.Context, dashboardUid string) (bool, error) GenerateNewPublicDashboardUid(ctx context.Context) (string, error) GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) GenerateNewPublicDashboardAccessToken(ctx context.Context) (string, error) @@ -39,7 +39,6 @@ type Store interface { GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error) GetPublicDashboardOrgId(ctx context.Context, accessToken string) (int64, error) ListPublicDashboards(ctx context.Context, orgId int64) ([]PublicDashboardListResponse, error) - PublicDashboardEnabled(ctx context.Context, dashboardUid string) (bool, error) SavePublicDashboardConfig(ctx context.Context, cmd SavePublicDashboardConfigCommand) error UpdatePublicDashboardConfig(ctx context.Context, cmd SavePublicDashboardConfigCommand) error } diff --git a/pkg/services/publicdashboards/queries/dashboard_queries.go b/pkg/services/publicdashboards/queries/dashboard_queries.go deleted file mode 100644 index 45665d2e692..00000000000 --- a/pkg/services/publicdashboards/queries/dashboard_queries.go +++ /dev/null @@ -1,102 +0,0 @@ -package queries - -import ( - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/expr" -) - -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 := GetDataSourceUidFromJson(panel) - - // if uid is for a mixed datasource, get the datasource uids from the targets - if uid == "-- Mixed --" { - for _, target := range panel.Get("targets").MustArray() { - target := simplejson.NewFromAny(target) - datasourceUid := target.Get("datasource").Get("uid").MustString() - if _, ok := exists[datasourceUid]; !ok { - datasourceUids = append(datasourceUids, datasourceUid) - exists[datasourceUid] = true - } - } - } else { - 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() { - panel := simplejson.NewFromAny(panelObj) - - var panelQueries []*simplejson.Json - - for _, queryObj := range panel.Get("targets").MustArray() { - query := simplejson.NewFromAny(queryObj) - - if hideAttr, exists := query.CheckGet("hide"); !exists || !hideAttr.MustBool() { - // We dont support exemplars for public dashboards currently - query.Del("exemplar") - - // if query target has no datasource, set it to have the datasource on the panel - if _, ok := query.CheckGet("datasource"); !ok { - uid := GetDataSourceUidFromJson(panel) - datasource := map[string]interface{}{"type": "public-ds", "uid": uid} - query.Set("datasource", datasource) - } - - panelQueries = append(panelQueries, query) - } - } - - result[panel.Get("id").MustInt64()] = panelQueries - } - - return result -} - -func HasExpressionQuery(queries []*simplejson.Json) bool { - for _, query := range queries { - uid := GetDataSourceUidFromJson(query) - if expr.IsDataSource(uid) { - return true - } - } - - return false -} - -func GetDataSourceUidFromJson(query *simplejson.Json) string { - uid := query.Get("datasource").Get("uid").MustString() - - // before 8.3 special types could be sent as datasource (expr) - if uid == "" { - uid = query.Get("datasource").MustString() - } - - return uid -} - -func SanitizeMetadataFromQueryData(res *backend.QueryDataResponse) { - for k := range res.Responses { - frames := res.Responses[k].Frames - for i := range frames { - if frames[i].Meta != nil { - frames[i].Meta.ExecutedQueryString = "" - frames[i].Meta.Custom = nil - } - } - } -} diff --git a/pkg/services/publicdashboards/queries/dashboard_queries_test.go b/pkg/services/publicdashboards/queries/dashboard_queries_test.go deleted file mode 100644 index a42857bbdc6..00000000000 --- a/pkg/services/publicdashboards/queries/dashboard_queries_test.go +++ /dev/null @@ -1,678 +0,0 @@ -package queries - -import ( - "testing" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/stretchr/testify/require" -) - -const ( - dashboardWithNoQueries = ` -{ - "panels": [ - { - "id": 2, - "title": "Panel Title", - "type": "timeseries" - } - ], - "schemaVersion": 35 -}` - - dashboardWithTargetsWithNoDatasources = ` -{ - "panels": [ - { - "id": 2, - "datasource": { - "type": "postgres", - "uid": "abc123" - }, - "targets": [ - { - "expr": "go_goroutines{job=\"$job\"}", - "interval": "", - "legendFormat": "", - "refId": "A" - }, - { - "exemplar": true, - "expr": "query2", - "interval": "", - "legendFormat": "", - "refId": "B" - } - ], - "title": "Panel Title", - "type": "timeseries" - } - ], - "schemaVersion": 35 -}` - - dashboardWithQueriesExemplarEnabled = ` -{ - "panels": [ - { - "id": 2, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "exemplar": true, - "expr": "go_goroutines{job=\"$job\"}", - "interval": "", - "legendFormat": "", - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "promds2" - }, - "exemplar": true, - "expr": "query2", - "interval": "", - "legendFormat": "", - "refId": "B" - } - ], - "title": "Panel Title", - "type": "timeseries" - } - ], - "schemaVersion": 35 -}` - - dashboardWithQueriesAndExpression = ` -{ - "panels": [ - { - "id": 2, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "exemplar": true, - "expr": "go_goroutines{job=\"$job\"}", - "interval": "", - "legendFormat": "", - "refId": "A" - }, - { - "datasource": { - "name": "Expression", - "type": "__expr__", - "uid": "__expr__" - }, - "expression": "$A + 1", - "hide": false, - "refId": "EXPRESSION", - "type": "math" - }, - { - "datasource": { - "type": "prometheus", - "uid": "promds2" - }, - "exemplar": true, - "expr": "query2", - "interval": "", - "legendFormat": "", - "refId": "B" - } - ], - "title": "Panel Title", - "type": "timeseries" - } - ], - "schemaVersion": 35 -}` - - dashboardWithMixedDatasource = ` -{ - "panels": [ - { - "datasource": { - "type": "datasource", - "uid": "-- Mixed --" - }, - "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 -}` - - 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": [ - { - "datasource": "_yxMP8Ynk", - "id": 2, - "targets": [ - { - "exemplar": true, - "expr": "go_goroutines{job=\"$job\"}", - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "title": "Panel Title", - "type": "timeseries" - } - ], - "schemaVersion": 21 -}` - - dashboardWithOneHiddenQuery = ` -{ - "panels": [ - { - "id": 2, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "exemplar": true, - "expr": "go_goroutines{job=\"$job\"}", - "interval": "", - "legendFormat": "", - "refId": "A", - "hide": true - }, - { - "datasource": { - "type": "prometheus", - "uid": "promds2" - }, - "exemplar": true, - "expr": "query2", - "interval": "", - "legendFormat": "", - "refId": "B" - } - ], - "title": "Panel Title", - "type": "timeseries" - } - ], - "schemaVersion": 35 -}` - dashboardWithAllHiddenQueries = ` -{ - "panels": [ - { - "id": 2, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "exemplar": true, - "expr": "go_goroutines{job=\"$job\"}", - "interval": "", - "legendFormat": "", - "refId": "A", - "hide": true - }, - { - "datasource": { - "type": "prometheus", - "uid": "promds2" - }, - "exemplar": true, - "expr": "query2", - "interval": "", - "legendFormat": "", - "refId": "B", - "hide": true - } - ], - "title": "Panel Title", - "type": "timeseries" - } - ], - "schemaVersion": 35 -}` -) - -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 unique datasource ids from dashboard with a mixed datasource", func(t *testing.T) { - json, err := simplejson.NewJson([]byte(dashboardWithMixedDatasource)) - 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 TestHasExpressionQuery(t *testing.T) { - t.Run("will return true when expression query exists", func(t *testing.T) { - json, err := simplejson.NewJson([]byte(dashboardWithQueriesAndExpression)) - require.NoError(t, err) - - queries := GroupQueriesByPanelId(json) - panelId := int64(2) - require.True(t, HasExpressionQuery(queries[panelId])) - }) - t.Run("will return false when no expression query exists", func(t *testing.T) { - json, err := simplejson.NewJson([]byte(dashboardWithMixedDatasource)) - require.NoError(t, err) - - queries := GroupQueriesByPanelId(json) - panelId := int64(2) - require.False(t, HasExpressionQuery(queries[panelId])) - }) -} - -func TestGroupQueriesByPanelId(t *testing.T) { - t.Run("can extract queries from dashboard with panel datasource string that has no datasource on panel targets", func(t *testing.T) { - json, err := simplejson.NewJson([]byte(oldStyleDashboard)) - require.NoError(t, err) - queries := GroupQueriesByPanelId(json) - - panelId := int64(2) - queriesByDatasource := groupQueriesByDataSource(t, queries[panelId]) - require.Len(t, queriesByDatasource[0], 1) - }) - t.Run("will delete exemplar property from target if exists", func(t *testing.T) { - json, err := simplejson.NewJson([]byte(dashboardWithQueriesExemplarEnabled)) - require.NoError(t, err) - queries := GroupQueriesByPanelId(json) - - panelId := int64(2) - queriesByDatasource := groupQueriesByDataSource(t, queries[panelId]) - for _, query := range queriesByDatasource[0] { - _, ok := query.CheckGet("exemplar") - require.False(t, ok) - } - }) - t.Run("can extract queries from dashboard with panel json datasource that has no datasource on panel targets", func(t *testing.T) { - json, err := simplejson.NewJson([]byte(dashboardWithTargetsWithNoDatasources)) - require.NoError(t, err) - queries := GroupQueriesByPanelId(json) - - panelId := int64(2) - queriesByDatasource := groupQueriesByDataSource(t, queries[panelId]) - require.Len(t, queriesByDatasource[0], 2) - }) - t.Run("can extract no queries from empty dashboard", func(t *testing.T) { - json, err := simplejson.NewJson([]byte(`{"panels": {}}`)) - require.NoError(t, err) - - queries := GroupQueriesByPanelId(json) - require.Len(t, queries, 0) - }) - - t.Run("can extract no queries from empty panel", func(t *testing.T) { - json, err := simplejson.NewJson([]byte(dashboardWithNoQueries)) - require.NoError(t, err) - - queries := GroupQueriesByPanelId(json) - require.Len(t, queries, 1) - require.Contains(t, queries, int64(2)) - require.Len(t, queries[2], 0) - }) - - t.Run("can extract queries from panels", func(t *testing.T) { - json, err := simplejson.NewJson([]byte(dashboardWithQueriesExemplarEnabled)) - require.NoError(t, err) - - queries := GroupQueriesByPanelId(json) - require.Len(t, queries, 1) - require.Contains(t, queries, int64(2)) - require.Len(t, queries[2], 2) - query, err := queries[2][0].MarshalJSON() - require.NoError(t, err) - require.JSONEq(t, `{ - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "expr": "go_goroutines{job=\"$job\"}", - "interval": "", - "legendFormat": "", - "refId": "A" - }`, string(query)) - query, err = queries[2][1].MarshalJSON() - require.NoError(t, err) - require.JSONEq(t, `{ - "datasource": { - "type": "prometheus", - "uid": "promds2" - }, - "expr": "query2", - "interval": "", - "legendFormat": "", - "refId": "B" - }`, string(query)) - }) - - t.Run("can extract queries from old-style panels", func(t *testing.T) { - json, err := simplejson.NewJson([]byte(oldStyleDashboard)) - require.NoError(t, err) - - queries := GroupQueriesByPanelId(json) - require.Len(t, queries, 1) - require.Contains(t, queries, int64(2)) - require.Len(t, queries[2], 1) - query, err := queries[2][0].MarshalJSON() - require.NoError(t, err) - require.JSONEq(t, `{ - "datasource": { - "uid": "_yxMP8Ynk", - "type": "public-ds" - }, - "expr": "go_goroutines{job=\"$job\"}", - "interval": "", - "legendFormat": "", - "refId": "A" - }`, string(query)) - }) - - t.Run("hidden query filtered", func(t *testing.T) { - json, err := simplejson.NewJson([]byte(dashboardWithOneHiddenQuery)) - require.NoError(t, err) - queries := GroupQueriesByPanelId(json)[2] - - require.Len(t, queries, 1) - for _, query := range queries { - if hideAttr, exists := query.CheckGet("hide"); exists && hideAttr.MustBool() { - require.Fail(t, "hidden queries should have been filtered") - } - } - }) - - t.Run("hidden query filtered, so empty queries returned", func(t *testing.T) { - json, err := simplejson.NewJson([]byte(dashboardWithAllHiddenQueries)) - require.NoError(t, err) - queries := GroupQueriesByPanelId(json)[2] - - require.Len(t, queries, 0) - }) -} - -func TestGroupQueriesByDataSource(t *testing.T) { - t.Run("can divide queries by datasource", func(t *testing.T) { - queries := []*simplejson.Json{ - simplejson.MustJson([]byte(`{ - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "exemplar": true, - "expr": "go_goroutines{job=\"$job\"}", - "interval": "", - "legendFormat": "", - "refId": "A" - }`)), - simplejson.MustJson([]byte(`{ - "datasource": { - "type": "prometheus", - "uid": "promds2" - }, - "exemplar": true, - "expr": "query2", - "interval": "", - "legendFormat": "", - "refId": "B" - }`)), - } - - queriesByDatasource := groupQueriesByDataSource(t, queries) - require.Len(t, queriesByDatasource, 2) - require.Contains(t, queriesByDatasource, []*simplejson.Json{simplejson.MustJson([]byte(`{ - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "exemplar": true, - "expr": "go_goroutines{job=\"$job\"}", - "interval": "", - "legendFormat": "", - "refId": "A" - }`))}) - require.Contains(t, queriesByDatasource, []*simplejson.Json{simplejson.MustJson([]byte(`{ - "datasource": { - "type": "prometheus", - "uid": "promds2" - }, - "exemplar": true, - "expr": "query2", - "interval": "", - "legendFormat": "", - "refId": "B" - }`))}) - }) -} - -func TestSanitizeMetadataFromQueryData(t *testing.T) { - t.Run("can remove metadata from query", func(t *testing.T) { - fakeResponse := &backend.QueryDataResponse{ - Responses: backend.Responses{ - "A": backend.DataResponse{ - Frames: data.Frames{ - &data.Frame{ - Name: "1", - Meta: &data.FrameMeta{ - ExecutedQueryString: "Test1", - Custom: map[string]string{ - "test1": "test1", - }, - }, - }, - &data.Frame{ - Name: "2", - Meta: &data.FrameMeta{ - ExecutedQueryString: "Test2", - Custom: map[string]string{ - "test2": "test2", - }, - }, - }, - }, - }, - "B": backend.DataResponse{ - Frames: data.Frames{ - &data.Frame{ - Name: "3", - Meta: &data.FrameMeta{ - ExecutedQueryString: "Test3", - Custom: map[string]string{ - "test3": "test3", - }, - }, - }, - }, - }, - }, - } - SanitizeMetadataFromQueryData(fakeResponse) - for k := range fakeResponse.Responses { - frames := fakeResponse.Responses[k].Frames - for i := range frames { - require.Empty(t, frames[i].Meta.ExecutedQueryString) - require.Empty(t, frames[i].Meta.Custom) - } - } - }) -} - -func groupQueriesByDataSource(t *testing.T, queries []*simplejson.Json) (result [][]*simplejson.Json) { - t.Helper() - byDataSource := make(map[string][]*simplejson.Json) - - for _, query := range queries { - uid := GetDataSourceUidFromJson(query) - byDataSource[uid] = append(byDataSource[uid], query) - } - - for _, queries := range byDataSource { - result = append(result, queries) - } - - return -} diff --git a/pkg/services/publicdashboards/service/query.go b/pkg/services/publicdashboards/service/query.go new file mode 100644 index 00000000000..a5de8394925 --- /dev/null +++ b/pkg/services/publicdashboards/service/query.go @@ -0,0 +1,288 @@ +package service + +import ( + "context" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/components/simplejson" + dashmodels "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/annotations" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/publicdashboards/models" + "github.com/grafana/grafana/pkg/services/publicdashboards/validation" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/tsdb/grafanads" +) + +// GetAnnotations returns annotations for a public dashboard +func (pd *PublicDashboardServiceImpl) GetAnnotations(ctx context.Context, reqDTO models.AnnotationsQueryDTO, accessToken string) ([]models.AnnotationEvent, error) { + _, dash, err := pd.GetPublicDashboard(ctx, accessToken) + if err != nil { + return nil, err + } + + annoDto, err := UnmarshalDashboardAnnotations(dash.Data) + if err != nil { + return nil, err + } + + anonymousUser := buildAnonymousUser(ctx, dash) + + uniqueEvents := make(map[int64]models.AnnotationEvent, 0) + for _, anno := range annoDto.Annotations.List { + // skip annotations that are not enabled or are not a grafana datasource + if !anno.Enable || (*anno.Datasource.Uid != grafanads.DatasourceUID && *anno.Datasource.Uid != grafanads.DatasourceName) { + continue + } + annoQuery := &annotations.ItemQuery{ + From: reqDTO.From, + To: reqDTO.To, + OrgId: dash.OrgId, + DashboardId: dash.Id, + DashboardUid: dash.Uid, + Limit: anno.Target.Limit, + MatchAny: anno.Target.MatchAny, + SignedInUser: anonymousUser, + } + + if anno.Target.Type == "tags" { + annoQuery.DashboardId = 0 + annoQuery.Tags = anno.Target.Tags + } + + annotationItems, err := pd.AnnotationsRepo.Find(ctx, annoQuery) + if err != nil { + return nil, err + } + + for _, item := range annotationItems { + event := models.AnnotationEvent{ + Id: item.Id, + DashboardId: item.DashboardId, + Tags: item.Tags, + IsRegion: item.TimeEnd > 0 && item.Time != item.TimeEnd, + Text: item.Text, + Color: *anno.IconColor, + Time: item.Time, + TimeEnd: item.TimeEnd, + Source: anno, + } + + // We want dashboard annotations to reference the panel they're for. If no panelId is provided, they'll show up on all panels + // which is only intended for tag and org annotations. + if anno.Type == "dashboard" { + event.PanelId = item.PanelId + } + + // We want events from tag queries to overwrite existing events + _, has := uniqueEvents[event.Id] + if !has || (has && anno.Target.Type == "tags") { + uniqueEvents[event.Id] = event + } + } + } + + var results []models.AnnotationEvent + for _, result := range uniqueEvents { + results = append(results, result) + } + + return results, nil +} + +// GetMetricRequest returns a metric request for the given panel and query +func (pd *PublicDashboardServiceImpl) GetMetricRequest(ctx context.Context, dashboard *dashmodels.Dashboard, publicDashboard *models.PublicDashboard, panelId int64, queryDto models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) { + err := validation.ValidateQueryPublicDashboardRequest(queryDto) + if err != nil { + return dtos.MetricRequest{}, err + } + + metricReqDTO, err := pd.buildMetricRequest( + ctx, + dashboard, + publicDashboard, + panelId, + queryDto, + ) + if err != nil { + return dtos.MetricRequest{}, err + } + + return metricReqDTO, nil +} + +// GetQueryDataResponse returns a query data response for the given panel and query +func (pd *PublicDashboardServiceImpl) GetQueryDataResponse(ctx context.Context, skipCache bool, queryDto models.PublicDashboardQueryDTO, panelId int64, accessToken string) (*backend.QueryDataResponse, error) { + publicDashboard, dashboard, err := pd.GetPublicDashboard(ctx, accessToken) + if err != nil { + return nil, err + } + + metricReq, err := pd.GetMetricRequest(ctx, dashboard, publicDashboard, panelId, queryDto) + if err != nil { + return nil, err + } + + if len(metricReq.Queries) == 0 { + return nil, nil + } + + anonymousUser := buildAnonymousUser(ctx, dashboard) + res, err := pd.QueryDataService.QueryData(ctx, anonymousUser, skipCache, metricReq) + + reqDatasources := metricReq.GetUniqueDatasourceTypes() + if err != nil { + LogQueryFailure(reqDatasources, pd.log, err) + return nil, err + } + LogQuerySuccess(reqDatasources, pd.log) + + sanitizeMetadataFromQueryData(res) + + return res, nil +} + +// buildMetricRequest merges public dashboard parameters with dashboard and returns a metrics request to be sent to query backend +func (pd *PublicDashboardServiceImpl) buildMetricRequest(ctx context.Context, dashboard *dashmodels.Dashboard, publicDashboard *models.PublicDashboard, panelId int64, reqDTO models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) { + // group queries by panel + queriesByPanel := groupQueriesByPanelId(dashboard.Data) + queries, ok := queriesByPanel[panelId] + if !ok { + return dtos.MetricRequest{}, models.ErrPublicDashboardPanelNotFound + } + + ts := publicDashboard.BuildTimeSettings(dashboard) + + // determine safe resolution to query data at + safeInterval, safeResolution := pd.getSafeIntervalAndMaxDataPoints(reqDTO, ts) + for i := range queries { + queries[i].Set("intervalMs", safeInterval) + queries[i].Set("maxDataPoints", safeResolution) + } + + return dtos.MetricRequest{ + From: ts.From, + To: ts.To, + Queries: queries, + }, nil +} + +// buildAnonymousUser creates a user with permissions to read from all datasources used in the dashboard +func buildAnonymousUser(ctx context.Context, dashboard *dashmodels.Dashboard) *user.SignedInUser { + datasourceUids := getUniqueDashboardDatasourceUids(dashboard.Data) + + // Create a user with blank permissions + anonymousUser := &user.SignedInUser{OrgID: dashboard.OrgId, Permissions: make(map[int64]map[string][]string)} + + // Scopes needed for Annotation queries + annotationScopes := []string{accesscontrol.ScopeAnnotationsTypeDashboard} + // Need to access all dashboards since tags annotations span across all dashboards + dashboardScopes := []string{dashboards.ScopeDashboardsProvider.GetResourceAllScope()} + + // Scopes needed for datasource queries + queryScopes := make([]string, 0) + readScopes := make([]string, 0) + for _, uid := range datasourceUids { + scope := datasources.ScopeProvider.GetResourceScopeUID(uid) + queryScopes = append(queryScopes, scope) + readScopes = append(readScopes, scope) + } + + // Apply all scopes to the actions we need the user to be able to perform + permissions := make(map[string][]string) + permissions[datasources.ActionQuery] = queryScopes + permissions[datasources.ActionRead] = readScopes + permissions[accesscontrol.ActionAnnotationsRead] = annotationScopes + permissions[dashboards.ActionDashboardsRead] = dashboardScopes + + anonymousUser.Permissions[dashboard.OrgId] = permissions + + return anonymousUser +} + +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 := getDataSourceUidFromJson(panel) + + // if uid is for a mixed datasource, get the datasource uids from the targets + if uid == "-- Mixed --" { + for _, target := range panel.Get("targets").MustArray() { + target := simplejson.NewFromAny(target) + datasourceUid := target.Get("datasource").Get("uid").MustString() + if _, ok := exists[datasourceUid]; !ok { + datasourceUids = append(datasourceUids, datasourceUid) + exists[datasourceUid] = true + } + } + } else { + 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() { + panel := simplejson.NewFromAny(panelObj) + + var panelQueries []*simplejson.Json + + for _, queryObj := range panel.Get("targets").MustArray() { + query := simplejson.NewFromAny(queryObj) + + if hideAttr, exists := query.CheckGet("hide"); !exists || !hideAttr.MustBool() { + // We dont support exemplars for public dashboards currently + query.Del("exemplar") + + // if query target has no datasource, set it to have the datasource on the panel + if _, ok := query.CheckGet("datasource"); !ok { + uid := getDataSourceUidFromJson(panel) + datasource := map[string]interface{}{"type": "public-ds", "uid": uid} + query.Set("datasource", datasource) + } + + panelQueries = append(panelQueries, query) + } + } + + result[panel.Get("id").MustInt64()] = panelQueries + } + + return result +} + +func getDataSourceUidFromJson(query *simplejson.Json) string { + uid := query.Get("datasource").Get("uid").MustString() + + // before 8.3 special types could be sent as datasource (expr) + if uid == "" { + uid = query.Get("datasource").MustString() + } + + return uid +} + +func sanitizeMetadataFromQueryData(res *backend.QueryDataResponse) { + for k := range res.Responses { + frames := res.Responses[k].Frames + for i := range frames { + if frames[i].Meta != nil { + frames[i].Meta.ExecutedQueryString = "" + frames[i].Meta.Custom = nil + } + } + } +} diff --git a/pkg/services/publicdashboards/service/query_test.go b/pkg/services/publicdashboards/service/query_test.go new file mode 100644 index 00000000000..1ce038cb37f --- /dev/null +++ b/pkg/services/publicdashboards/service/query_test.go @@ -0,0 +1,1252 @@ +package service + +import ( + "context" + "errors" + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/components/simplejson" + dashboard2 "github.com/grafana/grafana/pkg/coremodel/dashboard" + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/log" + grafanamodels "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/annotations" + "github.com/grafana/grafana/pkg/services/annotations/annotationsimpl" + dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database" + "github.com/grafana/grafana/pkg/services/featuremgmt" + . "github.com/grafana/grafana/pkg/services/publicdashboards" + "github.com/grafana/grafana/pkg/services/publicdashboards/database" + "github.com/grafana/grafana/pkg/services/publicdashboards/internal" + . "github.com/grafana/grafana/pkg/services/publicdashboards/models" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/tag/tagimpl" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tsdb/intervalv2" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +const ( + dashboardWithNoQueries = ` +{ + "panels": [ + { + "id": 2, + "title": "Panel Title", + "type": "timeseries" + } + ], + "schemaVersion": 35 +}` + + dashboardWithTargetsWithNoDatasources = ` +{ + "panels": [ + { + "id": 2, + "datasource": { + "type": "postgres", + "uid": "abc123" + }, + "targets": [ + { + "expr": "go_goroutines{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "A" + }, + { + "exemplar": true, + "expr": "query2", + "interval": "", + "legendFormat": "", + "refId": "B" + } + ], + "title": "Panel Title", + "type": "timeseries" + } + ], + "schemaVersion": 35 +}` + + dashboardWithQueriesExemplarEnabled = ` +{ + "panels": [ + { + "id": 2, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "_yxMP8Ynk" + }, + "exemplar": true, + "expr": "go_goroutines{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "promds2" + }, + "exemplar": true, + "expr": "query2", + "interval": "", + "legendFormat": "", + "refId": "B" + } + ], + "title": "Panel Title", + "type": "timeseries" + } + ], + "schemaVersion": 35 +}` + + dashboardWithMixedDatasource = ` +{ + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Mixed --" + }, + "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 +}` + + 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": [ + { + "datasource": "_yxMP8Ynk", + "id": 2, + "targets": [ + { + "exemplar": true, + "expr": "go_goroutines{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Panel Title", + "type": "timeseries" + } + ], + "schemaVersion": 21 +}` + + dashboardWithOneHiddenQuery = ` +{ + "panels": [ + { + "id": 2, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "_yxMP8Ynk" + }, + "exemplar": true, + "expr": "go_goroutines{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "A", + "hide": true + }, + { + "datasource": { + "type": "prometheus", + "uid": "promds2" + }, + "exemplar": true, + "expr": "query2", + "interval": "", + "legendFormat": "", + "refId": "B" + } + ], + "title": "Panel Title", + "type": "timeseries" + } + ], + "schemaVersion": 35 +}` + dashboardWithAllHiddenQueries = ` +{ + "panels": [ + { + "id": 2, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "_yxMP8Ynk" + }, + "exemplar": true, + "expr": "go_goroutines{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "A", + "hide": true + }, + { + "datasource": { + "type": "prometheus", + "uid": "promds2" + }, + "exemplar": true, + "expr": "query2", + "interval": "", + "legendFormat": "", + "refId": "B", + "hide": true + } + ], + "title": "Panel Title", + "type": "timeseries" + } + ], + "schemaVersion": 35 +}` +) + +func TestGetQueryDataResponse(t *testing.T) { + sqlStore := sqlstore.InitTestDB(t) + dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg)) + publicdashboardStore := database.ProvideStore(sqlStore) + + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: publicdashboardStore, + intervalCalculator: intervalv2.NewCalculator(), + } + + publicDashboardQueryDTO := PublicDashboardQueryDTO{ + IntervalMs: int64(1), + MaxDataPoints: int64(1), + } + + t.Run("Returns nil when query is hidden", func(t *testing.T) { + hiddenQuery := map[string]interface{}{ + "datasource": map[string]interface{}{ + "type": "mysql", + "uid": "ds1", + }, + "hide": true, + "refId": "A", + } + customPanels := []interface{}{ + map[string]interface{}{ + "id": 1, + "datasource": map[string]interface{}{ + "uid": "ds1", + }, + "targets": []interface{}{hiddenQuery}, + }} + + dashboard := insertTestDashboard(t, dashboardStore, "testDashWithHiddenQuery", 1, 0, true, []map[string]interface{}{}, customPanels) + dto := &SavePublicDashboardConfigDTO{ + DashboardUid: dashboard.Uid, + OrgId: dashboard.OrgId, + UserId: 7, + PublicDashboard: &PublicDashboard{ + IsEnabled: true, + DashboardUid: "NOTTHESAME", + OrgId: 9999999, + TimeSettings: timeSettings, + }, + } + pubdashDto, err := service.SavePublicDashboardConfig(context.Background(), SignedInUser, dto) + require.NoError(t, err) + + resp, _ := service.GetQueryDataResponse(context.Background(), true, publicDashboardQueryDTO, 1, pubdashDto.AccessToken) + require.Nil(t, resp) + }) +} + +func TestGetAnnotations(t *testing.T) { + t.Run("will build anonymous user with correct permissions to get annotations", func(t *testing.T) { + sqlStore := sqlstore.InitTestDB(t) + config := setting.NewCfg() + tagService := tagimpl.ProvideService(sqlStore, sqlStore.Cfg) + annotationsRepo := annotationsimpl.ProvideService(sqlStore, config, tagService) + fakeStore := FakePublicDashboardStore{} + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: &fakeStore, + AnnotationsRepo: annotationsRepo, + } + fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")). + Return(&PublicDashboard{Uid: "uid1", IsEnabled: true}, grafanamodels.NewDashboard("dash1"), nil) + reqDTO := AnnotationsQueryDTO{ + From: 1, + To: 2, + } + dash := grafanamodels.NewDashboard("testDashboard") + + items, _ := service.GetAnnotations(context.Background(), reqDTO, "abc123") + anonUser := buildAnonymousUser(context.Background(), dash) + + assert.Equal(t, "dashboards:*", anonUser.Permissions[0]["dashboards:read"][0]) + assert.Len(t, items, 0) + }) + + t.Run("Test events from tag queries overwrite built-in annotation queries and duplicate events are not returned", func(t *testing.T) { + dash := grafanamodels.NewDashboard("test") + color := "red" + name := "annoName" + grafanaAnnotation := DashAnnotation{ + Datasource: CreateDatasource("grafana", "grafana"), + Enable: true, + Name: &name, + IconColor: &color, + Target: &dashboard2.AnnotationTarget{ + Limit: 100, + MatchAny: false, + Tags: nil, + Type: "dashboard", + }, + Type: "dashboard", + } + grafanaTagAnnotation := DashAnnotation{ + Datasource: CreateDatasource("grafana", "grafana"), + Enable: true, + Name: &name, + IconColor: &color, + Target: &dashboard2.AnnotationTarget{ + Limit: 100, + MatchAny: false, + Tags: []string{"tag1"}, + Type: "tags", + }, + } + annos := []DashAnnotation{grafanaAnnotation, grafanaTagAnnotation} + dashboard := AddAnnotationsToDashboard(t, dash, annos) + + annotationsRepo := annotations.FakeAnnotationsRepo{} + fakeStore := FakePublicDashboardStore{} + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: &fakeStore, + AnnotationsRepo: &annotationsRepo, + } + pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid} + fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, dashboard, nil) + annotationsRepo.On("Find", mock.Anything, mock.Anything).Return([]*annotations.ItemDTO{ + { + Id: 1, + DashboardId: 1, + PanelId: 1, + Tags: []string{"tag1"}, + TimeEnd: 2, + Time: 2, + Text: "text", + }, + }, nil).Maybe() + + items, err := service.GetAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") + + expected := AnnotationEvent{ + Id: 1, + DashboardId: 1, + PanelId: 0, + Tags: []string{"tag1"}, + IsRegion: false, + Text: "text", + Color: color, + Time: 2, + TimeEnd: 2, + Source: grafanaTagAnnotation, + } + require.NoError(t, err) + assert.Len(t, items, 1) + assert.Equal(t, expected, items[0]) + }) + + t.Run("Test panelId set to zero when annotation event is for a tags query", func(t *testing.T) { + dash := grafanamodels.NewDashboard("test") + color := "red" + name := "annoName" + grafanaAnnotation := DashAnnotation{ + Datasource: CreateDatasource("grafana", "grafana"), + Enable: true, + Name: &name, + IconColor: &color, + Target: &dashboard2.AnnotationTarget{ + Limit: 100, + MatchAny: false, + Tags: []string{"tag1"}, + Type: "tags", + }, + } + annos := []DashAnnotation{grafanaAnnotation} + dashboard := AddAnnotationsToDashboard(t, dash, annos) + + annotationsRepo := annotations.FakeAnnotationsRepo{} + fakeStore := FakePublicDashboardStore{} + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: &fakeStore, + AnnotationsRepo: &annotationsRepo, + } + pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid} + fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, dashboard, nil) + annotationsRepo.On("Find", mock.Anything, mock.Anything).Return([]*annotations.ItemDTO{ + { + Id: 1, + DashboardId: 1, + PanelId: 1, + Tags: []string{}, + TimeEnd: 1, + Time: 2, + Text: "text", + }, + }, nil).Maybe() + + items, err := service.GetAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") + + expected := AnnotationEvent{ + Id: 1, + DashboardId: 1, + PanelId: 0, + Tags: []string{}, + IsRegion: true, + Text: "text", + Color: color, + Time: 2, + TimeEnd: 1, + Source: grafanaAnnotation, + } + require.NoError(t, err) + assert.Len(t, items, 1) + assert.Equal(t, expected, items[0]) + }) + + t.Run("Test can get grafana annotations and will skip annotation queries and disabled annotations", func(t *testing.T) { + dash := grafanamodels.NewDashboard("test") + color := "red" + name := "annoName" + disabledGrafanaAnnotation := DashAnnotation{ + Datasource: CreateDatasource("grafana", "grafana"), + Enable: false, + Name: &name, + IconColor: &color, + } + grafanaAnnotation := DashAnnotation{ + Datasource: CreateDatasource("grafana", "grafana"), + Enable: true, + Name: &name, + IconColor: &color, + Target: &dashboard2.AnnotationTarget{ + Limit: 100, + MatchAny: true, + Tags: nil, + Type: "dashboard", + }, + Type: "dashboard", + } + queryAnnotation := DashAnnotation{ + Datasource: CreateDatasource("prometheus", "abc123"), + Enable: true, + Name: &name, + } + annos := []DashAnnotation{grafanaAnnotation, queryAnnotation, disabledGrafanaAnnotation} + dashboard := AddAnnotationsToDashboard(t, dash, annos) + + annotationsRepo := annotations.FakeAnnotationsRepo{} + fakeStore := FakePublicDashboardStore{} + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: &fakeStore, + AnnotationsRepo: &annotationsRepo, + } + pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid} + fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, dashboard, nil) + annotationsRepo.On("Find", mock.Anything, mock.Anything).Return([]*annotations.ItemDTO{ + { + Id: 1, + DashboardId: 1, + PanelId: 1, + Tags: []string{}, + TimeEnd: 1, + Time: 2, + Text: "text", + }, + }, nil).Maybe() + + items, err := service.GetAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") + + expected := AnnotationEvent{ + Id: 1, + DashboardId: 1, + PanelId: 1, + Tags: []string{}, + IsRegion: true, + Text: "text", + Color: color, + Time: 2, + TimeEnd: 1, + Source: grafanaAnnotation, + } + require.NoError(t, err) + assert.Len(t, items, 1) + assert.Equal(t, expected, items[0]) + }) + + t.Run("test will return nothing when dashboard has no annotations", func(t *testing.T) { + annotationsRepo := annotations.FakeAnnotationsRepo{} + fakeStore := FakePublicDashboardStore{} + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: &fakeStore, + AnnotationsRepo: &annotationsRepo, + } + dashboard := grafanamodels.NewDashboard("dashWithNoAnnotations") + pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid} + fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, dashboard, nil) + + items, err := service.GetAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") + + require.NoError(t, err) + assert.Empty(t, items) + }) + + t.Run("test will error when annotations repo returns an error", func(t *testing.T) { + annotationsRepo := annotations.FakeAnnotationsRepo{} + fakeStore := FakePublicDashboardStore{} + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: &fakeStore, + AnnotationsRepo: &annotationsRepo, + } + dash := grafanamodels.NewDashboard("test") + color := "red" + name := "annoName" + grafanaAnnotation := DashAnnotation{ + Datasource: CreateDatasource("grafana", "grafana"), + Enable: true, + Name: &name, + IconColor: &color, + Target: &dashboard2.AnnotationTarget{ + Limit: 100, + MatchAny: false, + Tags: []string{"tag1"}, + Type: "tags", + }, + } + annos := []DashAnnotation{grafanaAnnotation} + dash = AddAnnotationsToDashboard(t, dash, annos) + pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dash.Uid} + fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, dash, nil) + annotationsRepo.On("Find", mock.Anything, mock.Anything).Return(nil, errors.New("failed")).Maybe() + + items, err := service.GetAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") + + require.Error(t, err) + require.Nil(t, items) + }) +} + +func TestGetMetricRequest(t *testing.T) { + sqlStore := db.InitTestDB(t) + dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg)) + publicdashboardStore := database.ProvideStore(sqlStore) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) + publicDashboard := &PublicDashboard{ + Uid: "1", + DashboardUid: dashboard.Uid, + IsEnabled: true, + AccessToken: "abc123", + } + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: publicdashboardStore, + intervalCalculator: intervalv2.NewCalculator(), + } + + t.Run("will return an error when validation fails", func(t *testing.T) { + publicDashboardQueryDTO := PublicDashboardQueryDTO{ + IntervalMs: int64(-1), + MaxDataPoints: int64(-1), + } + + _, err := service.GetMetricRequest(context.Background(), dashboard, publicDashboard, 1, publicDashboardQueryDTO) + + require.Error(t, err) + }) + + t.Run("will not return an error when validation succeeds", func(t *testing.T) { + publicDashboardQueryDTO := PublicDashboardQueryDTO{ + IntervalMs: int64(1), + MaxDataPoints: int64(1), + } + from, to := internal.GetTimeRangeFromDashboard(t, dashboard.Data) + + metricReq, err := service.GetMetricRequest(context.Background(), dashboard, publicDashboard, 1, publicDashboardQueryDTO) + + require.NoError(t, err) + require.Equal(t, from, metricReq.From) + require.Equal(t, to, metricReq.To) + }) +} + +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 unique datasource ids from dashboard with a mixed datasource", func(t *testing.T) { + json, err := simplejson.NewJson([]byte(dashboardWithMixedDatasource)) + 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 TestBuildMetricRequest(t *testing.T) { + sqlStore := db.InitTestDB(t) + dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg)) + publicdashboardStore := database.ProvideStore(sqlStore) + + publicDashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) + nonPublicDashboard := insertTestDashboard(t, dashboardStore, "testNonPublicDashie", 1, 0, true, []map[string]interface{}{}, nil) + from, to := internal.GetTimeRangeFromDashboard(t, publicDashboard.Data) + + service := &PublicDashboardServiceImpl{ + log: log.New("test.logger"), + store: publicdashboardStore, + intervalCalculator: intervalv2.NewCalculator(), + } + + publicDashboardQueryDTO := PublicDashboardQueryDTO{ + IntervalMs: int64(10000000), + MaxDataPoints: int64(200), + } + + dto := &SavePublicDashboardConfigDTO{ + DashboardUid: publicDashboard.Uid, + OrgId: publicDashboard.OrgId, + PublicDashboard: &PublicDashboard{ + IsEnabled: true, + DashboardUid: "NOTTHESAME", + OrgId: 9999999, + TimeSettings: timeSettings, + }, + } + + publicDashboardPD, err := service.SavePublicDashboardConfig(context.Background(), SignedInUser, dto) + require.NoError(t, err) + + nonPublicDto := &SavePublicDashboardConfigDTO{ + DashboardUid: nonPublicDashboard.Uid, + OrgId: nonPublicDashboard.OrgId, + PublicDashboard: &PublicDashboard{ + IsEnabled: false, + DashboardUid: "NOTTHESAME", + OrgId: 9999999, + TimeSettings: defaultPubdashTimeSettings, + }, + } + + _, err = service.SavePublicDashboardConfig(context.Background(), SignedInUser, nonPublicDto) + require.NoError(t, err) + + t.Run("extracts queries from provided dashboard", func(t *testing.T) { + reqDTO, err := service.buildMetricRequest( + context.Background(), + publicDashboard, + publicDashboardPD, + 1, + publicDashboardQueryDTO, + ) + require.NoError(t, err) + + require.Equal(t, from, reqDTO.From) + require.Equal(t, to, reqDTO.To) + + for i := range reqDTO.Queries { + require.Equal(t, publicDashboardQueryDTO.IntervalMs, reqDTO.Queries[i].Get("intervalMs").MustInt64()) + require.Equal(t, publicDashboardQueryDTO.MaxDataPoints, reqDTO.Queries[i].Get("maxDataPoints").MustInt64()) + } + + require.Len(t, reqDTO.Queries, 2) + + require.Equal( + t, + simplejson.NewFromAny(map[string]interface{}{ + "datasource": map[string]interface{}{ + "type": "mysql", + "uid": "ds1", + }, + "intervalMs": int64(10000000), + "maxDataPoints": int64(200), + "refId": "A", + }), + reqDTO.Queries[0], + ) + + require.Equal( + t, + simplejson.NewFromAny(map[string]interface{}{ + "datasource": map[string]interface{}{ + "type": "prometheus", + "uid": "ds2", + }, + "intervalMs": int64(10000000), + "maxDataPoints": int64(200), + "refId": "B", + }), + reqDTO.Queries[1], + ) + }) + + t.Run("returns an error when panel missing", func(t *testing.T) { + _, err := service.buildMetricRequest( + context.Background(), + publicDashboard, + publicDashboardPD, + 49, + publicDashboardQueryDTO, + ) + + require.ErrorContains(t, err, ErrPublicDashboardPanelNotFound.Reason) + }) + + t.Run("metric request built without hidden query", func(t *testing.T) { + hiddenQuery := map[string]interface{}{ + "datasource": map[string]interface{}{ + "type": "mysql", + "uid": "ds1", + }, + "hide": true, + "refId": "A", + } + nonHiddenQuery := map[string]interface{}{ + "datasource": map[string]interface{}{ + "type": "prometheus", + "uid": "ds2", + }, + "refId": "B", + } + + customPanels := []interface{}{ + map[string]interface{}{ + "id": 1, + "datasource": map[string]interface{}{ + "uid": "ds1", + }, + "targets": []interface{}{hiddenQuery, nonHiddenQuery}, + }} + + publicDashboard := insertTestDashboard(t, dashboardStore, "testDashWithHiddenQuery", 1, 0, true, []map[string]interface{}{}, customPanels) + + reqDTO, err := service.buildMetricRequest( + context.Background(), + publicDashboard, + publicDashboardPD, + 1, + publicDashboardQueryDTO, + ) + require.NoError(t, err) + + require.Equal(t, from, reqDTO.From) + require.Equal(t, to, reqDTO.To) + + for i := range reqDTO.Queries { + require.Equal(t, publicDashboardQueryDTO.IntervalMs, reqDTO.Queries[i].Get("intervalMs").MustInt64()) + require.Equal(t, publicDashboardQueryDTO.MaxDataPoints, reqDTO.Queries[i].Get("maxDataPoints").MustInt64()) + } + + require.Len(t, reqDTO.Queries, 1) + + require.NotEqual( + t, + simplejson.NewFromAny(hiddenQuery), + reqDTO.Queries[0], + ) + + require.Equal( + t, + simplejson.NewFromAny(nonHiddenQuery), + reqDTO.Queries[0], + ) + }) + + t.Run("metric request built with 0 queries len when all queries are hidden", func(t *testing.T) { + customPanels := []interface{}{ + map[string]interface{}{ + "id": 1, + "datasource": map[string]interface{}{ + "uid": "ds1", + }, + "targets": []interface{}{map[string]interface{}{ + "datasource": map[string]interface{}{ + "type": "mysql", + "uid": "ds1", + }, + "hide": true, + "refId": "A", + }, map[string]interface{}{ + "datasource": map[string]interface{}{ + "type": "prometheus", + "uid": "ds2", + }, + "hide": true, + "refId": "B", + }}, + }} + + publicDashboard := insertTestDashboard(t, dashboardStore, "testDashWithAllQueriesHidden", 1, 0, true, []map[string]interface{}{}, customPanels) + + reqDTO, err := service.buildMetricRequest( + context.Background(), + publicDashboard, + publicDashboardPD, + 1, + publicDashboardQueryDTO, + ) + require.NoError(t, err) + + require.Equal(t, from, reqDTO.From) + require.Equal(t, to, reqDTO.To) + + require.Len(t, reqDTO.Queries, 0) + }) +} + +func TestBuildAnonymousUser(t *testing.T) { + sqlStore := db.InitTestDB(t) + dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg)) + dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) + //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) { + user := buildAnonymousUser(context.Background(), dashboard) + + 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]) + }) + t.Run("will add dashboard and annotation permissions needed for getting annotations", func(t *testing.T) { + user := buildAnonymousUser(context.Background(), dashboard) + + require.Equal(t, dashboard.OrgId, user.OrgID) + require.Equal(t, "annotations:type:dashboard", user.Permissions[user.OrgID]["annotations:read"][0]) + require.Equal(t, "dashboards:*", user.Permissions[user.OrgID]["dashboards:read"][0]) + }) +} + +func TestGroupQueriesByPanelId(t *testing.T) { + t.Run("can extract queries from dashboard with panel datasource string that has no datasource on panel targets", func(t *testing.T) { + json, err := simplejson.NewJson([]byte(oldStyleDashboard)) + require.NoError(t, err) + queries := groupQueriesByPanelId(json) + + panelId := int64(2) + queriesByDatasource := groupQueriesByDataSource(t, queries[panelId]) + require.Len(t, queriesByDatasource[0], 1) + }) + t.Run("will delete exemplar property from target if exists", func(t *testing.T) { + json, err := simplejson.NewJson([]byte(dashboardWithQueriesExemplarEnabled)) + require.NoError(t, err) + queries := groupQueriesByPanelId(json) + + panelId := int64(2) + queriesByDatasource := groupQueriesByDataSource(t, queries[panelId]) + for _, query := range queriesByDatasource[0] { + _, ok := query.CheckGet("exemplar") + require.False(t, ok) + } + }) + t.Run("can extract queries from dashboard with panel json datasource that has no datasource on panel targets", func(t *testing.T) { + json, err := simplejson.NewJson([]byte(dashboardWithTargetsWithNoDatasources)) + require.NoError(t, err) + queries := groupQueriesByPanelId(json) + + panelId := int64(2) + queriesByDatasource := groupQueriesByDataSource(t, queries[panelId]) + require.Len(t, queriesByDatasource[0], 2) + }) + t.Run("can extract no queries from empty dashboard", func(t *testing.T) { + json, err := simplejson.NewJson([]byte(`{"panels": {}}`)) + require.NoError(t, err) + + queries := groupQueriesByPanelId(json) + require.Len(t, queries, 0) + }) + + t.Run("can extract no queries from empty panel", func(t *testing.T) { + json, err := simplejson.NewJson([]byte(dashboardWithNoQueries)) + require.NoError(t, err) + + queries := groupQueriesByPanelId(json) + require.Len(t, queries, 1) + require.Contains(t, queries, int64(2)) + require.Len(t, queries[2], 0) + }) + + t.Run("can extract queries from panels", func(t *testing.T) { + json, err := simplejson.NewJson([]byte(dashboardWithQueriesExemplarEnabled)) + require.NoError(t, err) + + queries := groupQueriesByPanelId(json) + require.Len(t, queries, 1) + require.Contains(t, queries, int64(2)) + require.Len(t, queries[2], 2) + query, err := queries[2][0].MarshalJSON() + require.NoError(t, err) + require.JSONEq(t, `{ + "datasource": { + "type": "prometheus", + "uid": "_yxMP8Ynk" + }, + "expr": "go_goroutines{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "A" + }`, string(query)) + query, err = queries[2][1].MarshalJSON() + require.NoError(t, err) + require.JSONEq(t, `{ + "datasource": { + "type": "prometheus", + "uid": "promds2" + }, + "expr": "query2", + "interval": "", + "legendFormat": "", + "refId": "B" + }`, string(query)) + }) + + t.Run("can extract queries from old-style panels", func(t *testing.T) { + json, err := simplejson.NewJson([]byte(oldStyleDashboard)) + require.NoError(t, err) + + queries := groupQueriesByPanelId(json) + require.Len(t, queries, 1) + require.Contains(t, queries, int64(2)) + require.Len(t, queries[2], 1) + query, err := queries[2][0].MarshalJSON() + require.NoError(t, err) + require.JSONEq(t, `{ + "datasource": { + "uid": "_yxMP8Ynk", + "type": "public-ds" + }, + "expr": "go_goroutines{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "A" + }`, string(query)) + }) + + t.Run("hidden query filtered", func(t *testing.T) { + json, err := simplejson.NewJson([]byte(dashboardWithOneHiddenQuery)) + require.NoError(t, err) + queries := groupQueriesByPanelId(json)[2] + + require.Len(t, queries, 1) + for _, query := range queries { + if hideAttr, exists := query.CheckGet("hide"); exists && hideAttr.MustBool() { + require.Fail(t, "hidden queries should have been filtered") + } + } + }) + + t.Run("hidden query filtered, so empty queries returned", func(t *testing.T) { + json, err := simplejson.NewJson([]byte(dashboardWithAllHiddenQueries)) + require.NoError(t, err) + queries := groupQueriesByPanelId(json)[2] + + require.Len(t, queries, 0) + }) +} + +func TestGroupQueriesByDataSource(t *testing.T) { + t.Run("can divide queries by datasource", func(t *testing.T) { + queries := []*simplejson.Json{ + simplejson.MustJson([]byte(`{ + "datasource": { + "type": "prometheus", + "uid": "_yxMP8Ynk" + }, + "exemplar": true, + "expr": "go_goroutines{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "A" + }`)), + simplejson.MustJson([]byte(`{ + "datasource": { + "type": "prometheus", + "uid": "promds2" + }, + "exemplar": true, + "expr": "query2", + "interval": "", + "legendFormat": "", + "refId": "B" + }`)), + } + + queriesByDatasource := groupQueriesByDataSource(t, queries) + require.Len(t, queriesByDatasource, 2) + require.Contains(t, queriesByDatasource, []*simplejson.Json{simplejson.MustJson([]byte(`{ + "datasource": { + "type": "prometheus", + "uid": "_yxMP8Ynk" + }, + "exemplar": true, + "expr": "go_goroutines{job=\"$job\"}", + "interval": "", + "legendFormat": "", + "refId": "A" + }`))}) + require.Contains(t, queriesByDatasource, []*simplejson.Json{simplejson.MustJson([]byte(`{ + "datasource": { + "type": "prometheus", + "uid": "promds2" + }, + "exemplar": true, + "expr": "query2", + "interval": "", + "legendFormat": "", + "refId": "B" + }`))}) + }) +} + +func TestSanitizeMetadataFromQueryData(t *testing.T) { + t.Run("can remove metadata from query", func(t *testing.T) { + fakeResponse := &backend.QueryDataResponse{ + Responses: backend.Responses{ + "A": backend.DataResponse{ + Frames: data.Frames{ + &data.Frame{ + Name: "1", + Meta: &data.FrameMeta{ + ExecutedQueryString: "Test1", + Custom: map[string]string{ + "test1": "test1", + }, + }, + }, + &data.Frame{ + Name: "2", + Meta: &data.FrameMeta{ + ExecutedQueryString: "Test2", + Custom: map[string]string{ + "test2": "test2", + }, + }, + }, + }, + }, + "B": backend.DataResponse{ + Frames: data.Frames{ + &data.Frame{ + Name: "3", + Meta: &data.FrameMeta{ + ExecutedQueryString: "Test3", + Custom: map[string]string{ + "test3": "test3", + }, + }, + }, + }, + }, + }, + } + sanitizeMetadataFromQueryData(fakeResponse) + for k := range fakeResponse.Responses { + frames := fakeResponse.Responses[k].Frames + for i := range frames { + require.Empty(t, frames[i].Meta.ExecutedQueryString) + require.Empty(t, frames[i].Meta.Custom) + } + } + }) +} + +func groupQueriesByDataSource(t *testing.T, queries []*simplejson.Json) (result [][]*simplejson.Json) { + t.Helper() + byDataSource := make(map[string][]*simplejson.Json) + + for _, query := range queries { + uid := getDataSourceUidFromJson(query) + byDataSource[uid] = append(byDataSource[uid], query) + } + + for _, queries := range byDataSource { + result = append(result, queries) + } + + return +} diff --git a/pkg/services/publicdashboards/service/service.go b/pkg/services/publicdashboards/service/service.go index 5eda3c65438..636e14489a2 100644 --- a/pkg/services/publicdashboards/service/service.go +++ b/pkg/services/publicdashboards/service/service.go @@ -6,21 +6,17 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" "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/services/publicdashboards/queries" "github.com/grafana/grafana/pkg/services/publicdashboards/validation" "github.com/grafana/grafana/pkg/services/query" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/tsdb/grafanads" "github.com/grafana/grafana/pkg/tsdb/intervalv2" "github.com/grafana/grafana/pkg/tsdb/legacydata" ) @@ -207,190 +203,6 @@ func (pd *PublicDashboardServiceImpl) updatePublicDashboardConfig(ctx context.Co return dto.PublicDashboard.Uid, pd.store.UpdatePublicDashboardConfig(ctx, cmd) } -func (pd *PublicDashboardServiceImpl) GetQueryDataResponse(ctx context.Context, skipCache bool, queryDto PublicDashboardQueryDTO, panelId int64, accessToken string) (*backend.QueryDataResponse, error) { - publicDashboard, dashboard, err := pd.GetPublicDashboard(ctx, accessToken) - if err != nil { - return nil, err - } - - metricReq, err := pd.GetMetricRequest(ctx, dashboard, publicDashboard, panelId, queryDto) - if err != nil { - return nil, err - } - - if len(metricReq.Queries) == 0 { - return nil, nil - } - - anonymousUser := pd.BuildAnonymousUser(ctx, dashboard) - res, err := pd.QueryDataService.QueryData(ctx, anonymousUser, skipCache, metricReq) - - reqDatasources := metricReq.GetUniqueDatasourceTypes() - if err != nil { - LogQueryFailure(reqDatasources, pd.log, err) - return nil, err - } - LogQuerySuccess(reqDatasources, pd.log) - - queries.SanitizeMetadataFromQueryData(res) - - return res, nil -} - -func (pd *PublicDashboardServiceImpl) GetMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64, queryDto PublicDashboardQueryDTO) (dtos.MetricRequest, error) { - err := validation.ValidateQueryPublicDashboardRequest(queryDto) - if err != nil { - return dtos.MetricRequest{}, err - } - - metricReqDTO, err := pd.buildMetricRequest( - ctx, - dashboard, - publicDashboard, - panelId, - queryDto, - ) - if err != nil { - return dtos.MetricRequest{}, err - } - - return metricReqDTO, nil -} - -func (pd *PublicDashboardServiceImpl) GetAnnotations(ctx context.Context, reqDTO AnnotationsQueryDTO, accessToken string) ([]AnnotationEvent, error) { - _, dash, err := pd.GetPublicDashboard(ctx, accessToken) - if err != nil { - return nil, err - } - - annoDto, err := UnmarshalDashboardAnnotations(dash.Data) - if err != nil { - return nil, err - } - - anonymousUser := pd.BuildAnonymousUser(ctx, dash) - - uniqueEvents := make(map[int64]AnnotationEvent, 0) - for _, anno := range annoDto.Annotations.List { - // skip annotations that are not enabled or are not a grafana datasource - if !anno.Enable || (*anno.Datasource.Uid != grafanads.DatasourceUID && *anno.Datasource.Uid != grafanads.DatasourceName) { - continue - } - annoQuery := &annotations.ItemQuery{ - From: reqDTO.From, - To: reqDTO.To, - OrgId: dash.OrgId, - DashboardId: dash.Id, - DashboardUid: dash.Uid, - Limit: anno.Target.Limit, - MatchAny: anno.Target.MatchAny, - SignedInUser: anonymousUser, - } - - if anno.Target.Type == "tags" { - annoQuery.DashboardId = 0 - annoQuery.Tags = anno.Target.Tags - } - - annotationItems, err := pd.AnnotationsRepo.Find(ctx, annoQuery) - if err != nil { - return nil, err - } - - for _, item := range annotationItems { - event := AnnotationEvent{ - Id: item.Id, - DashboardId: item.DashboardId, - Tags: item.Tags, - IsRegion: item.TimeEnd > 0 && item.Time != item.TimeEnd, - Text: item.Text, - Color: *anno.IconColor, - Time: item.Time, - TimeEnd: item.TimeEnd, - Source: anno, - } - - // We want dashboard annotations to reference the panel they're for. If no panelId is provided, they'll show up on all panels - // which is only intended for tag and org annotations. - if anno.Type == "dashboard" { - event.PanelId = item.PanelId - } - - // We want events from tag queries to overwrite existing events - _, has := uniqueEvents[event.Id] - if !has || (has && anno.Target.Type == "tags") { - uniqueEvents[event.Id] = event - } - } - } - - var results []AnnotationEvent - for _, result := range uniqueEvents { - results = append(results, result) - } - - return results, nil -} - -// buildMetricRequest merges public dashboard parameters with -// dashboard and returns a metrics request to be sent to query backend -func (pd *PublicDashboardServiceImpl) buildMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64, reqDTO PublicDashboardQueryDTO) (dtos.MetricRequest, error) { - // group queries by panel - queriesByPanel := queries.GroupQueriesByPanelId(dashboard.Data) - queries, ok := queriesByPanel[panelId] - if !ok { - return dtos.MetricRequest{}, ErrPublicDashboardPanelNotFound - } - - ts := publicDashboard.BuildTimeSettings(dashboard) - - // determine safe resolution to query data at - safeInterval, safeResolution := pd.getSafeIntervalAndMaxDataPoints(reqDTO, ts) - for i := range queries { - queries[i].Set("intervalMs", safeInterval) - queries[i].Set("maxDataPoints", safeResolution) - } - - return dtos.MetricRequest{ - From: ts.From, - To: ts.To, - Queries: queries, - }, nil -} - -// BuildAnonymousUser creates a user with permissions to read from all datasources used in the dashboard -func (pd *PublicDashboardServiceImpl) BuildAnonymousUser(ctx context.Context, dashboard *models.Dashboard) *user.SignedInUser { - datasourceUids := queries.GetUniqueDashboardDatasourceUids(dashboard.Data) - - // Create a user with blank permissions - anonymousUser := &user.SignedInUser{OrgID: dashboard.OrgId, Permissions: make(map[int64]map[string][]string)} - - // Scopes needed for Annotation queries - annotationScopes := []string{accesscontrol.ScopeAnnotationsTypeDashboard} - // Need to access all dashboards since tags annotations span across all dashboards - dashboardScopes := []string{dashboards.ScopeDashboardsProvider.GetResourceAllScope()} - - // Scopes needed for datasource queries - queryScopes := make([]string, 0) - readScopes := make([]string, 0) - for _, uid := range datasourceUids { - scope := datasources.ScopeProvider.GetResourceScopeUID(uid) - queryScopes = append(queryScopes, scope) - readScopes = append(readScopes, scope) - } - - // Apply all scopes to the actions we need the user to be able to perform - permissions := make(map[string][]string) - permissions[datasources.ActionQuery] = queryScopes - permissions[datasources.ActionRead] = readScopes - permissions[accesscontrol.ActionAnnotationsRead] = annotationScopes - permissions[dashboards.ActionDashboardsRead] = dashboardScopes - - anonymousUser.Permissions[dashboard.OrgId] = permissions - - return anonymousUser -} - // Gets a list of public dashboards by orgId func (pd *PublicDashboardServiceImpl) ListPublicDashboards(ctx context.Context, u *user.SignedInUser, orgId int64) ([]PublicDashboardListResponse, error) { publicDashboards, err := pd.store.ListPublicDashboards(ctx, orgId) @@ -402,11 +214,11 @@ func (pd *PublicDashboardServiceImpl) ListPublicDashboards(ctx context.Context, } func (pd *PublicDashboardServiceImpl) PublicDashboardEnabled(ctx context.Context, dashboardUid string) (bool, error) { - return pd.store.PublicDashboardEnabled(ctx, dashboardUid) + return pd.store.PublicDashboardEnabledExistsByDashboardUid(ctx, dashboardUid) } -func (pd *PublicDashboardServiceImpl) AccessTokenExists(ctx context.Context, accessToken string) (bool, error) { - return pd.store.AccessTokenExists(ctx, accessToken) +func (pd *PublicDashboardServiceImpl) PublicDashboardEnabledExistsByAccessToken(ctx context.Context, accessToken string) (bool, error) { + return pd.store.PublicDashboardEnabledExistsByAccessToken(ctx, accessToken) } func (pd *PublicDashboardServiceImpl) GetPublicDashboardOrgId(ctx context.Context, accessToken string) (int64, error) { diff --git a/pkg/services/publicdashboards/service/service_test.go b/pkg/services/publicdashboards/service/service_test.go index 9040a0624a4..3535323d239 100644 --- a/pkg/services/publicdashboards/service/service_test.go +++ b/pkg/services/publicdashboards/service/service_test.go @@ -3,7 +3,6 @@ package service import ( "context" "encoding/json" - "errors" "fmt" "testing" "time" @@ -14,25 +13,19 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/components/simplejson" - dashboard2 "github.com/grafana/grafana/pkg/coremodel/dashboard" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/annotations" - "github.com/grafana/grafana/pkg/services/annotations/annotationsimpl" "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" "github.com/grafana/grafana/pkg/services/publicdashboards/database" - "github.com/grafana/grafana/pkg/services/publicdashboards/internal" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" - "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/intervalv2" ) @@ -45,289 +38,6 @@ func TestLogPrefix(t *testing.T) { assert.Equal(t, LogPrefix, "publicdashboards.service") } -func TestGetAnnotations(t *testing.T) { - t.Run("will build anonymous user with correct permissions to get annotations", func(t *testing.T) { - sqlStore := sqlstore.InitTestDB(t) - config := setting.NewCfg() - tagService := tagimpl.ProvideService(sqlStore, sqlStore.Cfg) - annotationsRepo := annotationsimpl.ProvideService(sqlStore, config, tagService) - fakeStore := FakePublicDashboardStore{} - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: &fakeStore, - AnnotationsRepo: annotationsRepo, - } - fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")). - Return(&PublicDashboard{Uid: "uid1", IsEnabled: true}, models.NewDashboard("dash1"), nil) - reqDTO := AnnotationsQueryDTO{ - From: 1, - To: 2, - } - dash := models.NewDashboard("testDashboard") - - items, _ := service.GetAnnotations(context.Background(), reqDTO, "abc123") - anonUser := service.BuildAnonymousUser(context.Background(), dash) - - assert.Equal(t, "dashboards:*", anonUser.Permissions[0]["dashboards:read"][0]) - assert.Len(t, items, 0) - }) - - t.Run("Test events from tag queries overwrite built-in annotation queries and duplicate events are not returned", func(t *testing.T) { - dash := models.NewDashboard("test") - color := "red" - name := "annoName" - grafanaAnnotation := DashAnnotation{ - Datasource: CreateDatasource("grafana", "grafana"), - Enable: true, - Name: &name, - IconColor: &color, - Target: &dashboard2.AnnotationTarget{ - Limit: 100, - MatchAny: false, - Tags: nil, - Type: "dashboard", - }, - Type: "dashboard", - } - grafanaTagAnnotation := DashAnnotation{ - Datasource: CreateDatasource("grafana", "grafana"), - Enable: true, - Name: &name, - IconColor: &color, - Target: &dashboard2.AnnotationTarget{ - Limit: 100, - MatchAny: false, - Tags: []string{"tag1"}, - Type: "tags", - }, - } - annos := []DashAnnotation{grafanaAnnotation, grafanaTagAnnotation} - dashboard := AddAnnotationsToDashboard(t, dash, annos) - - annotationsRepo := annotations.FakeAnnotationsRepo{} - fakeStore := FakePublicDashboardStore{} - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: &fakeStore, - AnnotationsRepo: &annotationsRepo, - } - pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid} - fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, dashboard, nil) - annotationsRepo.On("Find", mock.Anything, mock.Anything).Return([]*annotations.ItemDTO{ - { - Id: 1, - DashboardId: 1, - PanelId: 1, - Tags: []string{"tag1"}, - TimeEnd: 2, - Time: 2, - Text: "text", - }, - }, nil).Maybe() - - items, err := service.GetAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") - - expected := AnnotationEvent{ - Id: 1, - DashboardId: 1, - PanelId: 0, - Tags: []string{"tag1"}, - IsRegion: false, - Text: "text", - Color: color, - Time: 2, - TimeEnd: 2, - Source: grafanaTagAnnotation, - } - require.NoError(t, err) - assert.Len(t, items, 1) - assert.Equal(t, expected, items[0]) - }) - - t.Run("Test panelId set to zero when annotation event is for a tags query", func(t *testing.T) { - dash := models.NewDashboard("test") - color := "red" - name := "annoName" - grafanaAnnotation := DashAnnotation{ - Datasource: CreateDatasource("grafana", "grafana"), - Enable: true, - Name: &name, - IconColor: &color, - Target: &dashboard2.AnnotationTarget{ - Limit: 100, - MatchAny: false, - Tags: []string{"tag1"}, - Type: "tags", - }, - } - annos := []DashAnnotation{grafanaAnnotation} - dashboard := AddAnnotationsToDashboard(t, dash, annos) - - annotationsRepo := annotations.FakeAnnotationsRepo{} - fakeStore := FakePublicDashboardStore{} - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: &fakeStore, - AnnotationsRepo: &annotationsRepo, - } - pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid} - fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, dashboard, nil) - annotationsRepo.On("Find", mock.Anything, mock.Anything).Return([]*annotations.ItemDTO{ - { - Id: 1, - DashboardId: 1, - PanelId: 1, - Tags: []string{}, - TimeEnd: 1, - Time: 2, - Text: "text", - }, - }, nil).Maybe() - - items, err := service.GetAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") - - expected := AnnotationEvent{ - Id: 1, - DashboardId: 1, - PanelId: 0, - Tags: []string{}, - IsRegion: true, - Text: "text", - Color: color, - Time: 2, - TimeEnd: 1, - Source: grafanaAnnotation, - } - require.NoError(t, err) - assert.Len(t, items, 1) - assert.Equal(t, expected, items[0]) - }) - - t.Run("Test can get grafana annotations and will skip annotation queries and disabled annotations", func(t *testing.T) { - dash := models.NewDashboard("test") - color := "red" - name := "annoName" - disabledGrafanaAnnotation := DashAnnotation{ - Datasource: CreateDatasource("grafana", "grafana"), - Enable: false, - Name: &name, - IconColor: &color, - } - grafanaAnnotation := DashAnnotation{ - Datasource: CreateDatasource("grafana", "grafana"), - Enable: true, - Name: &name, - IconColor: &color, - Target: &dashboard2.AnnotationTarget{ - Limit: 100, - MatchAny: true, - Tags: nil, - Type: "dashboard", - }, - Type: "dashboard", - } - queryAnnotation := DashAnnotation{ - Datasource: CreateDatasource("prometheus", "abc123"), - Enable: true, - Name: &name, - } - annos := []DashAnnotation{grafanaAnnotation, queryAnnotation, disabledGrafanaAnnotation} - dashboard := AddAnnotationsToDashboard(t, dash, annos) - - annotationsRepo := annotations.FakeAnnotationsRepo{} - fakeStore := FakePublicDashboardStore{} - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: &fakeStore, - AnnotationsRepo: &annotationsRepo, - } - pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid} - fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, dashboard, nil) - annotationsRepo.On("Find", mock.Anything, mock.Anything).Return([]*annotations.ItemDTO{ - { - Id: 1, - DashboardId: 1, - PanelId: 1, - Tags: []string{}, - TimeEnd: 1, - Time: 2, - Text: "text", - }, - }, nil).Maybe() - - items, err := service.GetAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") - - expected := AnnotationEvent{ - Id: 1, - DashboardId: 1, - PanelId: 1, - Tags: []string{}, - IsRegion: true, - Text: "text", - Color: color, - Time: 2, - TimeEnd: 1, - Source: grafanaAnnotation, - } - require.NoError(t, err) - assert.Len(t, items, 1) - assert.Equal(t, expected, items[0]) - }) - - t.Run("test will return nothing when dashboard has no annotations", func(t *testing.T) { - annotationsRepo := annotations.FakeAnnotationsRepo{} - fakeStore := FakePublicDashboardStore{} - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: &fakeStore, - AnnotationsRepo: &annotationsRepo, - } - dashboard := models.NewDashboard("dashWithNoAnnotations") - pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid} - fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, dashboard, nil) - - items, err := service.GetAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") - - require.NoError(t, err) - assert.Empty(t, items) - }) - - t.Run("test will error when annotations repo returns an error", func(t *testing.T) { - annotationsRepo := annotations.FakeAnnotationsRepo{} - fakeStore := FakePublicDashboardStore{} - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: &fakeStore, - AnnotationsRepo: &annotationsRepo, - } - dash := models.NewDashboard("test") - color := "red" - name := "annoName" - grafanaAnnotation := DashAnnotation{ - Datasource: CreateDatasource("grafana", "grafana"), - Enable: true, - Name: &name, - IconColor: &color, - Target: &dashboard2.AnnotationTarget{ - Limit: 100, - MatchAny: false, - Tags: []string{"tag1"}, - Type: "tags", - }, - } - annos := []DashAnnotation{grafanaAnnotation} - dash = AddAnnotationsToDashboard(t, dash, annos) - pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dash.Uid} - fakeStore.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, dash, nil) - annotationsRepo.On("Find", mock.Anything, mock.Anything).Return(nil, errors.New("failed")).Maybe() - - items, err := service.GetAnnotations(context.Background(), AnnotationsQueryDTO{}, "abc123") - - require.Error(t, err) - require.Nil(t, items) - }) -} - func TestGetPublicDashboard(t *testing.T) { type storeResp struct { pd *PublicDashboard @@ -655,342 +365,6 @@ func TestUpdatePublicDashboard(t *testing.T) { }) } -func TestBuildAnonymousUser(t *testing.T) { - sqlStore := db.InitTestDB(t) - dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg)) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) - 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) { - user := service.BuildAnonymousUser(context.Background(), dashboard) - - 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]) - }) - t.Run("will add dashboard and annotation permissions needed for getting annotations", func(t *testing.T) { - user := service.BuildAnonymousUser(context.Background(), dashboard) - - require.Equal(t, dashboard.OrgId, user.OrgID) - require.Equal(t, "annotations:type:dashboard", user.Permissions[user.OrgID]["annotations:read"][0]) - require.Equal(t, "dashboards:*", user.Permissions[user.OrgID]["dashboards:read"][0]) - }) -} - -func TestGetMetricRequest(t *testing.T) { - sqlStore := db.InitTestDB(t) - dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg)) - publicdashboardStore := database.ProvideStore(sqlStore) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) - publicDashboard := &PublicDashboard{ - Uid: "1", - DashboardUid: dashboard.Uid, - IsEnabled: true, - AccessToken: "abc123", - } - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: publicdashboardStore, - intervalCalculator: intervalv2.NewCalculator(), - } - - t.Run("will return an error when validation fails", func(t *testing.T) { - publicDashboardQueryDTO := PublicDashboardQueryDTO{ - IntervalMs: int64(-1), - MaxDataPoints: int64(-1), - } - - _, err := service.GetMetricRequest(context.Background(), dashboard, publicDashboard, 1, publicDashboardQueryDTO) - - require.Error(t, err) - }) - - t.Run("will not return an error when validation succeeds", func(t *testing.T) { - publicDashboardQueryDTO := PublicDashboardQueryDTO{ - IntervalMs: int64(1), - MaxDataPoints: int64(1), - } - from, to := internal.GetTimeRangeFromDashboard(t, dashboard.Data) - - metricReq, err := service.GetMetricRequest(context.Background(), dashboard, publicDashboard, 1, publicDashboardQueryDTO) - - require.NoError(t, err) - require.Equal(t, from, metricReq.From) - require.Equal(t, to, metricReq.To) - }) -} - -func TestGetQueryDataResponse(t *testing.T) { - sqlStore := sqlstore.InitTestDB(t) - dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg)) - publicdashboardStore := database.ProvideStore(sqlStore) - - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: publicdashboardStore, - intervalCalculator: intervalv2.NewCalculator(), - } - - publicDashboardQueryDTO := PublicDashboardQueryDTO{ - IntervalMs: int64(1), - MaxDataPoints: int64(1), - } - - t.Run("Returns nil when query is hidden", func(t *testing.T) { - hiddenQuery := map[string]interface{}{ - "datasource": map[string]interface{}{ - "type": "mysql", - "uid": "ds1", - }, - "hide": true, - "refId": "A", - } - customPanels := []interface{}{ - map[string]interface{}{ - "id": 1, - "datasource": map[string]interface{}{ - "uid": "ds1", - }, - "targets": []interface{}{hiddenQuery}, - }} - - dashboard := insertTestDashboard(t, dashboardStore, "testDashWithHiddenQuery", 1, 0, true, []map[string]interface{}{}, customPanels) - dto := &SavePublicDashboardConfigDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, - UserId: 7, - PublicDashboard: &PublicDashboard{ - IsEnabled: true, - DashboardUid: "NOTTHESAME", - OrgId: 9999999, - TimeSettings: timeSettings, - }, - } - pubdashDto, err := service.SavePublicDashboardConfig(context.Background(), SignedInUser, dto) - require.NoError(t, err) - - resp, _ := service.GetQueryDataResponse(context.Background(), true, publicDashboardQueryDTO, 1, pubdashDto.AccessToken) - require.Nil(t, resp) - }) -} - -func TestBuildMetricRequest(t *testing.T) { - sqlStore := db.InitTestDB(t) - dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg)) - publicdashboardStore := database.ProvideStore(sqlStore) - - publicDashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) - nonPublicDashboard := insertTestDashboard(t, dashboardStore, "testNonPublicDashie", 1, 0, true, []map[string]interface{}{}, nil) - from, to := internal.GetTimeRangeFromDashboard(t, publicDashboard.Data) - - service := &PublicDashboardServiceImpl{ - log: log.New("test.logger"), - store: publicdashboardStore, - intervalCalculator: intervalv2.NewCalculator(), - } - - publicDashboardQueryDTO := PublicDashboardQueryDTO{ - IntervalMs: int64(10000000), - MaxDataPoints: int64(200), - } - - dto := &SavePublicDashboardConfigDTO{ - DashboardUid: publicDashboard.Uid, - OrgId: publicDashboard.OrgId, - PublicDashboard: &PublicDashboard{ - IsEnabled: true, - DashboardUid: "NOTTHESAME", - OrgId: 9999999, - TimeSettings: timeSettings, - }, - } - - publicDashboardPD, err := service.SavePublicDashboardConfig(context.Background(), SignedInUser, dto) - require.NoError(t, err) - - nonPublicDto := &SavePublicDashboardConfigDTO{ - DashboardUid: nonPublicDashboard.Uid, - OrgId: nonPublicDashboard.OrgId, - PublicDashboard: &PublicDashboard{ - IsEnabled: false, - DashboardUid: "NOTTHESAME", - OrgId: 9999999, - TimeSettings: defaultPubdashTimeSettings, - }, - } - - _, err = service.SavePublicDashboardConfig(context.Background(), SignedInUser, nonPublicDto) - require.NoError(t, err) - - t.Run("extracts queries from provided dashboard", func(t *testing.T) { - reqDTO, err := service.buildMetricRequest( - context.Background(), - publicDashboard, - publicDashboardPD, - 1, - publicDashboardQueryDTO, - ) - require.NoError(t, err) - - require.Equal(t, from, reqDTO.From) - require.Equal(t, to, reqDTO.To) - - for i := range reqDTO.Queries { - require.Equal(t, publicDashboardQueryDTO.IntervalMs, reqDTO.Queries[i].Get("intervalMs").MustInt64()) - require.Equal(t, publicDashboardQueryDTO.MaxDataPoints, reqDTO.Queries[i].Get("maxDataPoints").MustInt64()) - } - - require.Len(t, reqDTO.Queries, 2) - - require.Equal( - t, - simplejson.NewFromAny(map[string]interface{}{ - "datasource": map[string]interface{}{ - "type": "mysql", - "uid": "ds1", - }, - "intervalMs": int64(10000000), - "maxDataPoints": int64(200), - "refId": "A", - }), - reqDTO.Queries[0], - ) - - require.Equal( - t, - simplejson.NewFromAny(map[string]interface{}{ - "datasource": map[string]interface{}{ - "type": "prometheus", - "uid": "ds2", - }, - "intervalMs": int64(10000000), - "maxDataPoints": int64(200), - "refId": "B", - }), - reqDTO.Queries[1], - ) - }) - - t.Run("returns an error when panel missing", func(t *testing.T) { - _, err := service.buildMetricRequest( - context.Background(), - publicDashboard, - publicDashboardPD, - 49, - publicDashboardQueryDTO, - ) - - require.ErrorContains(t, err, ErrPublicDashboardPanelNotFound.Reason) - }) - - t.Run("metric request built without hidden query", func(t *testing.T) { - hiddenQuery := map[string]interface{}{ - "datasource": map[string]interface{}{ - "type": "mysql", - "uid": "ds1", - }, - "hide": true, - "refId": "A", - } - nonHiddenQuery := map[string]interface{}{ - "datasource": map[string]interface{}{ - "type": "prometheus", - "uid": "ds2", - }, - "refId": "B", - } - - customPanels := []interface{}{ - map[string]interface{}{ - "id": 1, - "datasource": map[string]interface{}{ - "uid": "ds1", - }, - "targets": []interface{}{hiddenQuery, nonHiddenQuery}, - }} - - publicDashboard := insertTestDashboard(t, dashboardStore, "testDashWithHiddenQuery", 1, 0, true, []map[string]interface{}{}, customPanels) - - reqDTO, err := service.buildMetricRequest( - context.Background(), - publicDashboard, - publicDashboardPD, - 1, - publicDashboardQueryDTO, - ) - require.NoError(t, err) - - require.Equal(t, from, reqDTO.From) - require.Equal(t, to, reqDTO.To) - - for i := range reqDTO.Queries { - require.Equal(t, publicDashboardQueryDTO.IntervalMs, reqDTO.Queries[i].Get("intervalMs").MustInt64()) - require.Equal(t, publicDashboardQueryDTO.MaxDataPoints, reqDTO.Queries[i].Get("maxDataPoints").MustInt64()) - } - - require.Len(t, reqDTO.Queries, 1) - - require.NotEqual( - t, - simplejson.NewFromAny(hiddenQuery), - reqDTO.Queries[0], - ) - - require.Equal( - t, - simplejson.NewFromAny(nonHiddenQuery), - reqDTO.Queries[0], - ) - }) - - t.Run("metric request built with 0 queries len when all queries are hidden", func(t *testing.T) { - customPanels := []interface{}{ - map[string]interface{}{ - "id": 1, - "datasource": map[string]interface{}{ - "uid": "ds1", - }, - "targets": []interface{}{map[string]interface{}{ - "datasource": map[string]interface{}{ - "type": "mysql", - "uid": "ds1", - }, - "hide": true, - "refId": "A", - }, map[string]interface{}{ - "datasource": map[string]interface{}{ - "type": "prometheus", - "uid": "ds2", - }, - "hide": true, - "refId": "B", - }}, - }} - - publicDashboard := insertTestDashboard(t, dashboardStore, "testDashWithAllQueriesHidden", 1, 0, true, []map[string]interface{}{}, customPanels) - - reqDTO, err := service.buildMetricRequest( - context.Background(), - publicDashboard, - publicDashboardPD, - 1, - publicDashboardQueryDTO, - ) - require.NoError(t, err) - - require.Equal(t, from, reqDTO.From) - require.Equal(t, to, reqDTO.To) - - require.Len(t, reqDTO.Queries, 0) - }) -} - func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardStore, title string, orgId int64, folderId int64, isFolder bool, templateVars []map[string]interface{}, customPanels []interface{}, tags ...interface{}) *models.Dashboard { t.Helper()