mirror of https://github.com/grafana/grafana
Previews: datasource permissions (#52747)
* Previews: datasource permissions * lint * simplify - force non-null `ds_uids` * add `canBeDisabled` to search service * add `IncludeThumbnailsWithEmptyDsUids` * remove force refresh migration * refactor main preview service * add safeguard * revert ticker interval * update testdata * fix test * add mock search service * add datasources lookup test * update migration * extract ds lookup to its own package to avoid cyclic imports * lint * fix dashbaord extract, use the real datasource lookup in tests. IS IT BULLETPROOF YET?! * fix dashbaord extract, use the real datasource lookup in tests. IS IT BULLETPROOF YET?! * remove stale log * consistent casing * pass context to `createServiceAccount` * filter out the special grafana dspull/52928/merge
parent
d0e548c3e5
commit
18daa6754c
@ -0,0 +1,136 @@ |
||||
package dslookup |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
) |
||||
|
||||
type DataSourceRef struct { |
||||
UID string `json:"uid,omitempty"` |
||||
Type string `json:"type,omitempty"` |
||||
} |
||||
|
||||
type DatasourceLookup interface { |
||||
// ByRef will return the default DS given empty reference (nil ref, or empty ref.uid and ref.type)
|
||||
ByRef(ref *DataSourceRef) *DataSourceRef |
||||
ByType(dsType string) []DataSourceRef |
||||
} |
||||
|
||||
type DatasourceQueryResult struct { |
||||
UID string `xorm:"uid"` |
||||
Type string `xorm:"type"` |
||||
Name string `xorm:"name"` |
||||
IsDefault bool `xorm:"is_default"` |
||||
} |
||||
|
||||
func CreateDatasourceLookup(rows []*DatasourceQueryResult) DatasourceLookup { |
||||
byUID := make(map[string]*DataSourceRef, 50) |
||||
byName := make(map[string]*DataSourceRef, 50) |
||||
byType := make(map[string][]DataSourceRef, 50) |
||||
var defaultDS *DataSourceRef |
||||
|
||||
for _, row := range rows { |
||||
ref := &DataSourceRef{ |
||||
UID: row.UID, |
||||
Type: row.Type, |
||||
} |
||||
byUID[row.UID] = ref |
||||
byName[row.Name] = ref |
||||
if row.IsDefault { |
||||
defaultDS = ref |
||||
} |
||||
|
||||
if _, ok := byType[row.Type]; !ok { |
||||
byType[row.Type] = make([]DataSourceRef, 0) |
||||
} |
||||
byType[row.Type] = append(byType[row.Type], *ref) |
||||
} |
||||
|
||||
grafanaDs := &DataSourceRef{ |
||||
UID: "grafana", |
||||
Type: "datasource", |
||||
} |
||||
if defaultDS == nil { |
||||
// fallback replicated from /pkg/api/frontendsettings.go
|
||||
// https://github.com/grafana/grafana/blob/7ef21662f9ad74b80d832b9f2aa9db2fb4192741/pkg/api/frontendsettings.go#L51-L56
|
||||
defaultDS = grafanaDs |
||||
} |
||||
|
||||
if _, ok := byUID[grafanaDs.UID]; !ok { |
||||
byUID[grafanaDs.UID] = grafanaDs |
||||
} |
||||
|
||||
grafanaDsName := "-- Grafana --" |
||||
if _, ok := byName[grafanaDsName]; !ok { |
||||
byName[grafanaDsName] = grafanaDs |
||||
} |
||||
|
||||
return &DsLookup{ |
||||
byName: byName, |
||||
byUID: byUID, |
||||
byType: byType, |
||||
defaultDS: defaultDS, |
||||
} |
||||
} |
||||
|
||||
type DsLookup struct { |
||||
byName map[string]*DataSourceRef |
||||
byUID map[string]*DataSourceRef |
||||
byType map[string][]DataSourceRef |
||||
defaultDS *DataSourceRef |
||||
} |
||||
|
||||
func (d *DsLookup) ByRef(ref *DataSourceRef) *DataSourceRef { |
||||
if ref == nil { |
||||
return d.defaultDS |
||||
} |
||||
|
||||
key := "" |
||||
if ref.UID != "" { |
||||
ds, ok := d.byUID[ref.UID] |
||||
if ok { |
||||
return ds |
||||
} |
||||
key = ref.UID |
||||
} |
||||
if key == "" { |
||||
return d.defaultDS |
||||
} |
||||
ds, ok := d.byUID[key] |
||||
if ok { |
||||
return ds |
||||
} |
||||
|
||||
return d.byName[key] |
||||
} |
||||
|
||||
func (d *DsLookup) ByType(dsType string) []DataSourceRef { |
||||
ds, ok := d.byType[dsType] |
||||
if !ok { |
||||
return make([]DataSourceRef, 0) |
||||
} |
||||
|
||||
return ds |
||||
} |
||||
|
||||
func LoadDatasourceLookup(ctx context.Context, orgID int64, sql *sqlstore.SQLStore) (DatasourceLookup, error) { |
||||
rows := make([]*DatasourceQueryResult, 0) |
||||
|
||||
if err := sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { |
||||
sess.Table("data_source"). |
||||
Where("org_id = ?", orgID). |
||||
Cols("uid", "name", "type", "is_default") |
||||
|
||||
err := sess.Find(&rows) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
}); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return CreateDatasourceLookup(rows), nil |
||||
} |
@ -1,43 +1,36 @@ |
||||
package extract |
||||
|
||||
type DatasourceLookup interface { |
||||
// ByRef will return the default DS given empty reference (nil ref, or empty ref.uid and ref.type)
|
||||
ByRef(ref *DataSourceRef) *DataSourceRef |
||||
ByType(dsType string) []DataSourceRef |
||||
} |
||||
|
||||
type DataSourceRef struct { |
||||
UID string `json:"uid,omitempty"` |
||||
Type string `json:"type,omitempty"` |
||||
} |
||||
import ( |
||||
"github.com/grafana/grafana/pkg/services/searchV2/dslookup" |
||||
) |
||||
|
||||
type PanelInfo struct { |
||||
ID int64 `json:"id"` |
||||
Title string `json:"title"` |
||||
Description string `json:"description,omitempty"` |
||||
Type string `json:"type,omitempty"` // PluginID
|
||||
PluginVersion string `json:"pluginVersion,omitempty"` |
||||
Datasource []DataSourceRef `json:"datasource,omitempty"` // UIDs
|
||||
Transformer []string `json:"transformer,omitempty"` // ids of the transformation steps
|
||||
ID int64 `json:"id"` |
||||
Title string `json:"title"` |
||||
Description string `json:"description,omitempty"` |
||||
Type string `json:"type,omitempty"` // PluginID
|
||||
PluginVersion string `json:"pluginVersion,omitempty"` |
||||
Datasource []dslookup.DataSourceRef `json:"datasource,omitempty"` // UIDs
|
||||
Transformer []string `json:"transformer,omitempty"` // ids of the transformation steps
|
||||
|
||||
// Rows define panels as sub objects
|
||||
Collapsed []PanelInfo `json:"collapsed,omitempty"` |
||||
} |
||||
|
||||
type DashboardInfo struct { |
||||
UID string `json:"uid,omitempty"` |
||||
ID int64 `json:"id,omitempty"` // internal ID
|
||||
Title string `json:"title"` |
||||
Description string `json:"description,omitempty"` |
||||
Tags []string `json:"tags"` |
||||
TemplateVars []string `json:"templateVars,omitempty"` // the keys used
|
||||
Datasource []DataSourceRef `json:"datasource,omitempty"` // UIDs
|
||||
Panels []PanelInfo `json:"panels"` // nesed documents
|
||||
SchemaVersion int64 `json:"schemaVersion"` |
||||
LinkCount int64 `json:"linkCount"` |
||||
TimeFrom string `json:"timeFrom"` |
||||
TimeTo string `json:"timeTo"` |
||||
TimeZone string `json:"timezone"` |
||||
Refresh string `json:"refresh,omitempty"` |
||||
ReadOnly bool `json:"readOnly,omitempty"` // editable = false
|
||||
UID string `json:"uid,omitempty"` |
||||
ID int64 `json:"id,omitempty"` // internal ID
|
||||
Title string `json:"title"` |
||||
Description string `json:"description,omitempty"` |
||||
Tags []string `json:"tags"` |
||||
TemplateVars []string `json:"templateVars,omitempty"` // the keys used
|
||||
Datasource []dslookup.DataSourceRef `json:"datasource,omitempty"` // UIDs
|
||||
Panels []PanelInfo `json:"panels"` // nesed documents
|
||||
SchemaVersion int64 `json:"schemaVersion"` |
||||
LinkCount int64 `json:"linkCount"` |
||||
TimeFrom string `json:"timeFrom"` |
||||
TimeTo string `json:"timeTo"` |
||||
TimeZone string `json:"timezone"` |
||||
Refresh string `json:"refresh,omitempty"` |
||||
ReadOnly bool `json:"readOnly,omitempty"` // editable = false
|
||||
} |
||||
|
@ -0,0 +1,70 @@ |
||||
// Code generated by mockery v2.10.6. DO NOT EDIT.
|
||||
|
||||
package searchV2 |
||||
|
||||
import ( |
||||
context "context" |
||||
|
||||
backend "github.com/grafana/grafana-plugin-sdk-go/backend" |
||||
|
||||
mock "github.com/stretchr/testify/mock" |
||||
) |
||||
|
||||
// MockSearchService is an autogenerated mock type for the SearchService type
|
||||
type MockSearchService struct { |
||||
mock.Mock |
||||
} |
||||
|
||||
// DoDashboardQuery provides a mock function with given fields: ctx, user, orgId, query
|
||||
func (_m *MockSearchService) DoDashboardQuery(ctx context.Context, user *backend.User, orgId int64, query DashboardQuery) *backend.DataResponse { |
||||
ret := _m.Called(ctx, user, orgId, query) |
||||
|
||||
var r0 *backend.DataResponse |
||||
if rf, ok := ret.Get(0).(func(context.Context, *backend.User, int64, DashboardQuery) *backend.DataResponse); ok { |
||||
r0 = rf(ctx, user, orgId, query) |
||||
} else { |
||||
if ret.Get(0) != nil { |
||||
r0 = ret.Get(0).(*backend.DataResponse) |
||||
} |
||||
} |
||||
|
||||
return r0 |
||||
} |
||||
|
||||
// IsDisabled provides a mock function with given fields:
|
||||
func (_m *MockSearchService) IsDisabled() bool { |
||||
ret := _m.Called() |
||||
|
||||
var r0 bool |
||||
if rf, ok := ret.Get(0).(func() bool); ok { |
||||
r0 = rf() |
||||
} else { |
||||
r0 = ret.Get(0).(bool) |
||||
} |
||||
|
||||
return r0 |
||||
} |
||||
|
||||
// RegisterDashboardIndexExtender provides a mock function with given fields: ext
|
||||
func (_m *MockSearchService) RegisterDashboardIndexExtender(ext DashboardIndexExtender) { |
||||
_m.Called(ext) |
||||
} |
||||
|
||||
// Run provides a mock function with given fields: ctx
|
||||
func (_m *MockSearchService) Run(ctx context.Context) error { |
||||
ret := _m.Called(ctx) |
||||
|
||||
var r0 error |
||||
if rf, ok := ret.Get(0).(func(context.Context) error); ok { |
||||
r0 = rf(ctx) |
||||
} else { |
||||
r0 = ret.Error(0) |
||||
} |
||||
|
||||
return r0 |
||||
} |
||||
|
||||
// TriggerReIndex provides a mock function with given fields:
|
||||
func (_m *MockSearchService) TriggerReIndex() { |
||||
_m.Called() |
||||
} |
@ -0,0 +1,91 @@ |
||||
package thumbs |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend" |
||||
"github.com/grafana/grafana/pkg/services/featuremgmt" |
||||
"github.com/grafana/grafana/pkg/services/searchV2" |
||||
"github.com/grafana/grafana/pkg/tsdb/grafanads" |
||||
) |
||||
|
||||
type getDatasourceUidsForDashboard func(ctx context.Context, dashboardUid string, orgId int64) ([]string, error) |
||||
|
||||
type dsUidsLookup struct { |
||||
searchService searchV2.SearchService |
||||
crawlerAuth CrawlerAuth |
||||
features featuremgmt.FeatureToggles |
||||
} |
||||
|
||||
func getDatasourceUIDs(resp *backend.DataResponse, uid string) ([]string, error) { |
||||
if resp == nil { |
||||
return nil, errors.New("nil response") |
||||
} |
||||
|
||||
if resp.Error != nil { |
||||
return nil, resp.Error |
||||
} |
||||
|
||||
if len(resp.Frames) == 0 { |
||||
return nil, errors.New("empty response") |
||||
} |
||||
|
||||
frame := resp.Frames[0] |
||||
field, idx := frame.FieldByName("ds_uid") |
||||
|
||||
if field.Len() == 0 || idx == -1 { |
||||
return nil, fmt.Errorf("no ds_uid field for uid %s", uid) |
||||
} |
||||
|
||||
rawValue, ok := field.At(0).(json.RawMessage) |
||||
if !ok || rawValue == nil { |
||||
return nil, fmt.Errorf("invalid value for uid %s in ds_uid field: %s", uid, field.At(0)) |
||||
} |
||||
|
||||
jsonValue, err := rawValue.MarshalJSON() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
var uids []string |
||||
err = json.Unmarshal(jsonValue, &uids) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return uids, nil |
||||
} |
||||
|
||||
func filterOutGrafanaDs(uids []string) []string { |
||||
var filtered []string |
||||
for _, uid := range uids { |
||||
if uid != grafanads.DatasourceUID { |
||||
filtered = append(filtered, uid) |
||||
} |
||||
} |
||||
|
||||
return filtered |
||||
} |
||||
|
||||
func (d *dsUidsLookup) getDatasourceUidsForDashboard(ctx context.Context, dashboardUid string, orgId int64) ([]string, error) { |
||||
if d.searchService.IsDisabled() { |
||||
return nil, nil |
||||
} |
||||
|
||||
dashQueryResponse := d.searchService.DoDashboardQuery(ctx, &backend.User{ |
||||
Login: d.crawlerAuth.GetLogin(orgId), |
||||
Role: string(d.crawlerAuth.GetOrgRole()), |
||||
}, orgId, searchV2.DashboardQuery{ |
||||
UIDs: []string{dashboardUid}, |
||||
}) |
||||
|
||||
uids, err := getDatasourceUIDs(dashQueryResponse, dashboardUid) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return filterOutGrafanaDs(uids), nil |
||||
} |
@ -0,0 +1,57 @@ |
||||
package thumbs |
||||
|
||||
import ( |
||||
"context" |
||||
_ "embed" |
||||
"testing" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend" |
||||
"github.com/grafana/grafana-plugin-sdk-go/data" |
||||
"github.com/grafana/grafana/pkg/services/featuremgmt" |
||||
"github.com/grafana/grafana/pkg/services/searchV2" |
||||
"github.com/stretchr/testify/mock" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
var ( |
||||
//go:embed testdata/search_response_frame.json
|
||||
exampleListFrameJSON string |
||||
exampleListFrame = &data.Frame{} |
||||
_ = exampleListFrame.UnmarshalJSON([]byte(exampleListFrameJSON)) |
||||
) |
||||
|
||||
func TestShouldParseUidFromSearchResponseFrame(t *testing.T) { |
||||
searchService := &searchV2.MockSearchService{} |
||||
dsLookup := &dsUidsLookup{ |
||||
searchService: searchService, |
||||
crawlerAuth: &crawlerAuth{}, |
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch), |
||||
} |
||||
|
||||
dashboardUid := "abc" |
||||
searchService.On("IsDisabled").Return(false) |
||||
searchService.On("DoDashboardQuery", mock.Anything, mock.Anything, mock.Anything, searchV2.DashboardQuery{ |
||||
UIDs: []string{dashboardUid}, |
||||
}).Return(&backend.DataResponse{ |
||||
Frames: []*data.Frame{exampleListFrame}, |
||||
}) |
||||
|
||||
uids, err := dsLookup.getDatasourceUidsForDashboard(context.Background(), dashboardUid, 1) |
||||
require.NoError(t, err) |
||||
require.Equal(t, []string{"datasource-2", "datasource-3", "datasource-4"}, uids) |
||||
} |
||||
|
||||
func TestShouldReturnNullIfSearchServiceIsDisabled(t *testing.T) { |
||||
searchService := &searchV2.MockSearchService{} |
||||
dsLookup := &dsUidsLookup{ |
||||
searchService: searchService, |
||||
crawlerAuth: &crawlerAuth{}, |
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch), |
||||
} |
||||
|
||||
dashboardUid := "abc" |
||||
searchService.On("IsDisabled").Return(true) |
||||
uids, err := dsLookup.getDatasourceUidsForDashboard(context.Background(), dashboardUid, 1) |
||||
require.NoError(t, err) |
||||
require.Nil(t, uids) |
||||
} |
@ -0,0 +1,123 @@ |
||||
{ |
||||
"schema": { |
||||
"name": "Query results", |
||||
"refId": "Search", |
||||
"meta": { |
||||
"type": "search-results", |
||||
"custom": { |
||||
"count": 106, |
||||
"locationInfo": { |
||||
"yboVMzb7z": { |
||||
"name": "gdev dashboards", |
||||
"kind": "folder", |
||||
"url": "/dashboards/f/yboVMzb7z/gdev-dashboards" |
||||
} |
||||
}, |
||||
"sortBy": "name_sort" |
||||
} |
||||
}, |
||||
"fields": [ |
||||
{ |
||||
"name": "kind", |
||||
"type": "string", |
||||
"typeInfo": { |
||||
"frame": "string" |
||||
} |
||||
}, |
||||
{ |
||||
"name": "uid", |
||||
"type": "string", |
||||
"typeInfo": { |
||||
"frame": "string" |
||||
} |
||||
}, |
||||
{ |
||||
"name": "name", |
||||
"type": "string", |
||||
"typeInfo": { |
||||
"frame": "string" |
||||
} |
||||
}, |
||||
{ |
||||
"name": "panel_type", |
||||
"type": "string", |
||||
"typeInfo": { |
||||
"frame": "string" |
||||
} |
||||
}, |
||||
{ |
||||
"name": "url", |
||||
"type": "string", |
||||
"typeInfo": { |
||||
"frame": "string" |
||||
}, |
||||
"config": { |
||||
"links": [ |
||||
{ |
||||
"title": "link", |
||||
"url": "${__value.text}" |
||||
} |
||||
] |
||||
} |
||||
}, |
||||
{ |
||||
"name": "tags", |
||||
"type": "other", |
||||
"typeInfo": { |
||||
"frame": "json.RawMessage", |
||||
"nullable": true |
||||
} |
||||
}, |
||||
{ |
||||
"name": "ds_uid", |
||||
"type": "other", |
||||
"typeInfo": { |
||||
"frame": "json.RawMessage", |
||||
"nullable": false |
||||
} |
||||
}, |
||||
{ |
||||
"name": "location", |
||||
"type": "string", |
||||
"typeInfo": { |
||||
"frame": "string" |
||||
} |
||||
} |
||||
] |
||||
}, |
||||
"data": { |
||||
"values": [ |
||||
[ |
||||
"dashboard" |
||||
], |
||||
[ |
||||
"ujaM1h6nz" |
||||
], |
||||
[ |
||||
"abc2" |
||||
], |
||||
[ |
||||
"" |
||||
], |
||||
[ |
||||
"/dashboards/f/ujaM1h6nz/abc2" |
||||
], |
||||
[ |
||||
[ |
||||
"gdev" |
||||
] |
||||
], |
||||
[ |
||||
[ |
||||
"datasource-2", |
||||
"datasource-3", |
||||
"datasource-4", |
||||
"grafana" |
||||
] |
||||
], |
||||
[ |
||||
"" |
||||
] |
||||
] |
||||
} |
||||
} |
Loading…
Reference in new issue