From 90eb499b781ca94ed39390f93515aa543c25b08d Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Thu, 13 Feb 2025 17:17:14 +0100 Subject: [PATCH] PublicDashboards: Fetch dashboard as Grafana (#100344) --- pkg/apimachinery/identity/context.go | 2 + .../publicdashboards/service/query.go | 104 +--- .../publicdashboards/service/query_test.go | 457 +----------------- .../publicdashboards/service/service.go | 7 +- 4 files changed, 25 insertions(+), 545 deletions(-) diff --git a/pkg/apimachinery/identity/context.go b/pkg/apimachinery/identity/context.go index 627cace5d61..81a7f81de6c 100644 --- a/pkg/apimachinery/identity/context.go +++ b/pkg/apimachinery/identity/context.go @@ -75,12 +75,14 @@ func getWildcardPermissions(actions ...string) map[string][]string { // serviceIdentityPermissions is a list of wildcard permissions for provided actions. // We should add every action required "internally" here. var serviceIdentityPermissions = getWildcardPermissions( + "annotations:read", "folders:read", "folders:write", "folders:create", "dashboards:read", "dashboards:write", "dashboards:create", + "datasources:query", "datasources:read", "alert.provisioning:write", "alert.provisioning.secrets:read", diff --git a/pkg/services/publicdashboards/service/query.go b/pkg/services/publicdashboards/service/query.go index 3d8731e895d..446f74ab3b6 100644 --- a/pkg/services/publicdashboards/service/query.go +++ b/pkg/services/publicdashboards/service/query.go @@ -8,16 +8,13 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/expr" - "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/featuremgmt" "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" ) @@ -37,8 +34,8 @@ func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDT return nil, models.ErrInternalServerError.Errorf("FindAnnotations: failed to unmarshal dashboard annotations: %w", err) } - anonymousUser := buildAnonymousUser(ctx, dash, pd.features) - + // We don't have a signed in user for public dashboards. We are using Grafana's Identity to query the annotations. + svcCtx, svcIdent := identity.WithServiceIdentity(ctx, dash.OrgID) 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 @@ -51,7 +48,7 @@ func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDT OrgID: dash.OrgID, DashboardID: dash.ID, DashboardUID: dash.UID, - SignedInUser: anonymousUser, + SignedInUser: svcIdent, } if anno.Target != nil { @@ -63,7 +60,7 @@ func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDT } } - annotationItems, err := pd.AnnotationsRepo.Find(ctx, annoQuery) + annotationItems, err := pd.AnnotationsRepo.Find(svcCtx, annoQuery) if err != nil { return nil, models.ErrInternalServerError.Errorf("FindAnnotations: failed to find annotations: %w", err) } @@ -139,8 +136,9 @@ func (pd *PublicDashboardServiceImpl) GetQueryDataResponse(ctx context.Context, return nil, models.ErrPanelQueriesNotFound.Errorf("GetQueryDataResponse: failed to extract queries from panel") } - anonymousUser := buildAnonymousUser(ctx, dashboard, pd.features) - res, err := pd.QueryDataService.QueryData(ctx, anonymousUser, skipDSCache, metricReq) + // We don't have a signed in user for public dashboards. We are using Grafana's Identity to query the datasource. + svcCtx, svcIdent := identity.WithServiceIdentity(ctx, dashboard.OrgID) + res, err := pd.QueryDataService.QueryData(svcCtx, svcIdent, skipDSCache, metricReq) reqDatasources := metricReq.GetUniqueDatasourceTypes() if err != nil { @@ -180,92 +178,6 @@ func (pd *PublicDashboardServiceImpl) buildMetricRequest(dashboard *dashboards.D }, nil } -// buildAnonymousUser creates a user with permissions to read from all datasources used in the dashboard -func buildAnonymousUser(ctx context.Context, dashboard *dashboards.Dashboard, features featuremgmt.FeatureToggles) *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[dashboards.ActionDashboardsRead] = dashboardScopes - permissions[accesscontrol.ActionAnnotationsRead] = annotationScopes - - if features.IsEnabled(ctx, featuremgmt.FlagAnnotationPermissionUpdate) { - permissions[accesscontrol.ActionAnnotationsRead] = dashboardScopes - } - - anonymousUser.Permissions[dashboard.OrgID] = permissions - - return anonymousUser -} - -func getUniqueDashboardDatasourceUids(dashboard *simplejson.Json) []string { - var datasourceUids []string - exists := map[string]bool{} - - // collapsed rows contain panels in a nested structure, so we need to flatten them before calculate unique uids - flattenedPanels := getFlattenedPanels(dashboard) - - for _, panelObj := range flattenedPanels { - 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 _, targetObj := range panel.Get("targets").MustArray() { - target := simplejson.NewFromAny(targetObj) - datasourceUid := getDataSourceUidFromJson(target) - 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 getFlattenedPanels(dashboard *simplejson.Json) []any { - var flatPanels []any - for _, panelObj := range dashboard.Get("panels").MustArray() { - panel := simplejson.NewFromAny(panelObj) - // if the panel is a row and it is collapsed, get the queries from the panels inside the row - // if it is not collapsed, the row does not have any panels - if panel.Get("type").MustString() == "row" { - if panel.Get("collapsed").MustBool() { - flatPanels = append(flatPanels, panel.Get("panels").MustArray()...) - } - } else { - flatPanels = append(flatPanels, panelObj) - } - } - return flatPanels -} - func groupQueriesByPanelId(dashboard *simplejson.Json) map[int64][]*simplejson.Json { result := make(map[int64][]*simplejson.Json) diff --git a/pkg/services/publicdashboards/service/query_test.go b/pkg/services/publicdashboards/service/query_test.go index f6aec672eac..0db1dfd08b0 100644 --- a/pkg/services/publicdashboards/service/query_test.go +++ b/pkg/services/publicdashboards/service/query_test.go @@ -11,8 +11,8 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/infra/db" dashboard2 "github.com/grafana/grafana/pkg/kinds/dashboard" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/dashboards" @@ -110,161 +110,6 @@ const ( "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" - }, - { - "datasource": "6SOeCRrVk", - "exemplar": true, - "expr": "test{id=\"f0dd9b69-ad04-4342-8e79-ced8c245683b\", name=\"test\"}", - "hide": false, - "interval": "", - "legendFormat": "", - "refId": "B" - } - ], - "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": [ @@ -460,218 +305,6 @@ const ( ], "schemaVersion": 35 }` - - dashboardWithCollapsedRows = ` -{ -"panels": [ - { - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 12, - "title": "Row title", - "type": "row" - }, - { - "datasource": { - "type": "prometheus", - "uid": "qCbTUC37k" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 1 - }, - "id": 11, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "qCbTUC37k" - }, - "editorMode": "builder", - "expr": "access_evaluation_duration_bucket", - "legendFormat": "__auto", - "range": true, - "refId": "A" - } - ], - "title": "Panel Title", - "type": "timeseries" - }, - { - "collapsed": true, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 9 - }, - "id": 10, - "panels": [ - { - "datasource": { - "type": "influxdb", - "uid": "P49A45DF074423DFB" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 9, - "w": 12, - "x": 0, - "y": 10 - }, - "id": 8, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "9.4.0-pre", - "targets": [ - { - "datasource": { - "type": "influxdb", - "uid": "P49A45DF074423DFB" - }, - "query": "// v.bucket, v.timeRangeStart, and v.timeRange stop are all variables supported by the flux plugin and influxdb\nfrom(bucket: v.bucket)\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_value\"] >= 10 and r[\"_value\"] <= 20)", - "refId": "A" - } - ], - "title": "Panel Title", - "type": "timeseries" - } - ], - "title": "Row title 1", - "type": "row" - } - ] -}` ) func TestGetQueryDataResponse(t *testing.T) { @@ -731,8 +364,7 @@ func TestGetQueryDataResponse(t *testing.T) { func TestFindAnnotations(t *testing.T) { color := "red" name := "annoName" - features := featuremgmt.WithFeatures(featuremgmt.FlagAnnotationPermissionUpdate) - t.Run("will build anonymous user with correct permissions to get annotations", func(t *testing.T) { + t.Run("service identity has correct permissions to get annotations dashboards and query datasources", func(t *testing.T) { fakeStore := &FakePublicDashboardStore{} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")). Return(&PublicDashboard{Uid: "uid1", IsEnabled: true}, nil) @@ -746,11 +378,14 @@ func TestFindAnnotations(t *testing.T) { } dash := dashboards.NewDashboard("testDashboard") - items, _ := service.FindAnnotations(context.Background(), reqDTO, "abc123") - anonUser := buildAnonymousUser(context.Background(), dash, features) - - assert.Equal(t, "dashboards:*", anonUser.Permissions[0]["dashboards:read"][0]) + items, err := service.FindAnnotations(context.Background(), reqDTO, "abc123") + require.NoError(t, err) assert.Len(t, items, 0) + + _, svcIdent := identity.WithServiceIdentity(context.Background(), dash.OrgID) + require.Equal(t, "*", svcIdent.GetPermissions()["datasources:query"][0]) + require.Equal(t, "*", svcIdent.GetPermissions()["dashboards:read"][0]) + require.Equal(t, "*", svcIdent.GetPermissions()["annotations:read"][0]) }) t.Run("Test events from tag queries overwrite built-in annotation queries and duplicate events are not returned", func(t *testing.T) { @@ -1121,47 +756,6 @@ func TestGetMetricRequest(t *testing.T) { }) } -func TestGetUniqueDashboardDatasourceUids(t *testing.T) { - t.Run("can get unique datasource ids from dashboard", func(t *testing.T) { - json, err := simplejson.NewJson([]byte(dashboardWithDuplicateDatasources)) - require.NoError(t, err) - - uids := getUniqueDashboardDatasourceUids(json) - require.Len(t, uids, 2) - require.Equal(t, "abc123", uids[0]) - require.Equal(t, "_yxMP8Ynk", uids[1]) - }) - - t.Run("can get 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, 3) - require.Equal(t, "abc123", uids[0]) - require.Equal(t, "6SOeCRrVk", uids[1]) - require.Equal(t, "_yxMP8Ynk", uids[2]) - }) - - 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) - }) - - t.Run("can get unique datasource ids from dashboard with rows", func(t *testing.T) { - json, err := simplejson.NewJson([]byte(dashboardWithCollapsedRows)) - require.NoError(t, err) - - uids := getUniqueDashboardDatasourceUids(json) - require.Len(t, uids, 2) - require.Equal(t, "qCbTUC37k", uids[0]) - require.Equal(t, "P49A45DF074423DFB", uids[1]) - }) -} - func TestBuildMetricRequest(t *testing.T) { fakeDashboardService := &dashboards.FakeDashboardService{} service, sqlStore, cfg := newPublicDashboardServiceImpl(t, nil, nil, nil, fakeDashboardService, nil) @@ -1318,39 +912,6 @@ func TestBuildMetricRequest(t *testing.T) { }) } -func TestBuildAnonymousUser(t *testing.T) { - sqlStore, cfg := db.InitTestDBWithCfg(t) - dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore)) - require.NoError(t, err) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]interface{}{}, nil) - features := featuremgmt.WithFeatures() - - 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, features) - - 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, features) - - 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]) - }) - t.Run("will add dashboard and annotation permissions needed for getting annotations when FlagAnnotationPermissionUpdate is enabled", func(t *testing.T) { - features = featuremgmt.WithFeatures(featuremgmt.FlagAnnotationPermissionUpdate) - user := buildAnonymousUser(context.Background(), dashboard, features) - - require.Equal(t, dashboard.OrgID, user.OrgID) - require.Equal(t, "dashboards:*", 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)) diff --git a/pkg/services/publicdashboards/service/service.go b/pkg/services/publicdashboards/service/service.go index d3239e2857d..9223981518c 100644 --- a/pkg/services/publicdashboards/service/service.go +++ b/pkg/services/publicdashboards/service/service.go @@ -13,6 +13,7 @@ import ( "go.opentelemetry.io/otel" "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -136,7 +137,11 @@ func (pd *PublicDashboardServiceImpl) Find(ctx context.Context, uid string) (*Pu func (pd *PublicDashboardServiceImpl) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error) { ctx, span := tracer.Start(ctx, "publicdashboards.FindDashboard") defer span.End() - dash, err := pd.dashboardService.GetDashboard(ctx, &dashboards.GetDashboardQuery{UID: dashboardUid, OrgID: orgId}) + + // We don't have a signed in user for public dashboards. We are using Grafana's Identity to query the dashboard. + dash, err := identity.WithServiceIdentityFn(ctx, orgId, func(ctx context.Context) (*dashboards.Dashboard, error) { + return pd.dashboardService.GetDashboard(ctx, &dashboards.GetDashboardQuery{UID: dashboardUid, OrgID: orgId}) + }) if err != nil { var dashboardErr dashboards.DashboardErr if ok := errors.As(err, &dashboardErr); ok {