PublicDashboards: Fetch dashboard as Grafana (#100344)

This commit is contained in:
Gabriel MABILLE
2025-02-13 17:17:14 +01:00
committed by GitHub
parent d719e6c621
commit 90eb499b78
4 changed files with 25 additions and 545 deletions
+2
View File
@@ -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",
+8 -96
View File
@@ -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)
@@ -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))
@@ -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 {