diff --git a/pkg/services/searchV2/allowed_actions.go b/pkg/services/searchV2/allowed_actions.go new file mode 100644 index 00000000000..46d0b24ab43 --- /dev/null +++ b/pkg/services/searchV2/allowed_actions.go @@ -0,0 +1,257 @@ +package searchV2 + +import ( + "context" + "encoding/json" + "errors" + "sort" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/datasources" +) + +func (s *StandardSearchService) addAllowedActionsField(ctx context.Context, orgId int64, user *models.SignedInUser, response *backend.DataResponse) error { + references, err := getEntityReferences(response) + if err != nil { + return err + } + + allAllowedActions, err := s.createAllowedActions(ctx, orgId, user, references) + if err != nil { + return err + } + + if len(response.Frames) == 0 { + return errors.New("empty response") + } + + frame := response.Frames[0] + + allowedActionsField := data.NewFieldFromFieldType(data.FieldTypeJSON, len(allAllowedActions)) + allowedActionsField.Name = "allowed_actions" + frame.Fields = append(frame.Fields, allowedActionsField) + + for i, actions := range allAllowedActions { + js, _ := json.Marshal(actions) + jsb := json.RawMessage(js) + allowedActionsField.Set(i, jsb) + } + + return nil +} + +type allowedActions struct { + EntityKind entityKind `json:"kind"` + UID string `json:"uid"` + Actions []string `json:"actions"` +} + +func (s *StandardSearchService) createAllowedActions(ctx context.Context, orgId int64, user *models.SignedInUser, references []entityReferences) ([][]allowedActions, error) { + uidsPerKind := make(map[entityKind][]string) + for _, refs := range references { + if _, ok := uidsPerKind[refs.entityKind]; !ok { + uidsPerKind[refs.entityKind] = []string{} + } + + uidsPerKind[refs.entityKind] = append(uidsPerKind[refs.entityKind], refs.uid) + + if len(refs.dsUids) > 0 { + if _, ok := uidsPerKind[entityKindDatasource]; !ok { + uidsPerKind[entityKindDatasource] = []string{} + } + + uidsPerKind[entityKindDatasource] = append(uidsPerKind[entityKindDatasource], refs.dsUids...) + } + } + + allowedActionsByUid := make(map[entityKind]map[string][]string) + + for entKind, uids := range uidsPerKind { + if entKind == entityKindPanel { + emptyAllowedActions := make(map[string][]string) + for _, uid := range uids { + emptyAllowedActions[uid] = []string{} + } + allowedActionsByUid[entityKindPanel] = emptyAllowedActions + } + + var prefix string + switch entKind { + case entityKindFolder: + prefix = dashboards.ScopeFoldersPrefix + case entityKindDatasource: + prefix = datasources.ScopePrefix + case entityKindDashboard: + prefix = dashboards.ScopeDashboardsPrefix + default: + continue + } + + allowedActionsByUid[entKind] = s.getAllowedActionsByUid(ctx, user, orgId, prefix, uids) + } + + dsActionsByUid, ok := allowedActionsByUid[entityKindDatasource] + if !ok { + dsActionsByUid = make(map[string][]string) + } + + var out [][]allowedActions + for _, ref := range references { + var actions []allowedActions + + selfActions := make([]string, 0) + if selfKindActions, ok := allowedActionsByUid[ref.entityKind]; ok { + if self, ok := selfKindActions[ref.uid]; ok && len(self) > 0 { + selfActions = self + } + } + + actions = append(actions, allowedActions{ + EntityKind: ref.entityKind, + UID: ref.uid, + Actions: selfActions, + }) + + for _, dsUid := range ref.dsUids { + dsActions := make([]string, 0) + if dsAct, ok := dsActionsByUid[dsUid]; ok { + dsActions = dsAct + } + + actions = append(actions, allowedActions{ + EntityKind: entityKindDatasource, + UID: dsUid, + Actions: dsActions, + }) + } + + out = append(out, actions) + } + + return out, nil +} + +func (s *StandardSearchService) getAllowedActionsByUid(ctx context.Context, user *models.SignedInUser, + orgID int64, prefix string, resourceIDs []string) map[string][]string { + if s.ac.IsDisabled() { + return map[string][]string{} + } + + if user.Permissions == nil { + return map[string][]string{} + } + + permissions, ok := user.Permissions[orgID] + if !ok { + return map[string][]string{} + } + + uidsAsMap := make(map[string]bool) + for _, uid := range resourceIDs { + uidsAsMap[uid] = true + } + + out := make(map[string][]string) + resp := accesscontrol.GetResourcesMetadata(ctx, permissions, prefix, uidsAsMap) + for uid, meta := range resp { + var actions []string + for action := range meta { + actions = append(actions, action) + } + sort.Strings(actions) + out[uid] = actions + } + return out +} + +type entityReferences struct { + entityKind entityKind + uid string + dsUids []string +} + +func getEntityReferences(resp *backend.DataResponse) ([]entityReferences, 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] + + kindField, idx := frame.FieldByName("kind") + if idx == -1 { + return nil, errors.New("no kind field") + } + + dsUidField, idx := frame.FieldByName("ds_uid") + if idx == -1 { + return nil, errors.New("no ds_uid field") + } + uidField, idx := frame.FieldByName("uid") + if idx == -1 { + return nil, errors.New("no dash_uid field") + } + + if dsUidField.Len() != uidField.Len() { + return nil, errors.New("mismatched lengths") + } + + var out []entityReferences + for i := 0; i < dsUidField.Len(); i++ { + kind, ok := kindField.At(i).(string) + if !ok || kind == "" { + return nil, errors.New("invalid value in kind field") + } + + uid, ok := uidField.At(i).(string) + if !ok || uid == "" { + return nil, errors.New("invalid value in uid field") + } + + if entityKind(kind) != entityKindDashboard { + out = append(out, entityReferences{ + entityKind: entityKind(kind), + uid: uid, + }) + continue + } + + uidField, ok := uidField.At(i).(string) + if !ok || uidField == "" { + return nil, errors.New("invalid value in dash_uid field") + } + + rawDsUids, ok := dsUidField.At(i).(*json.RawMessage) + if !ok { + return nil, errors.New("invalid value in ds_uid field") + } + + var uids []string + if rawDsUids != nil { + jsonValue, err := rawDsUids.MarshalJSON() + if err != nil { + return nil, err + } + + err = json.Unmarshal(jsonValue, &uids) + if err != nil { + return nil, err + } + } + + out = append(out, entityReferences{entityKind: entityKindDashboard, uid: uid, dsUids: uids}) + } + + return out, nil +} diff --git a/pkg/services/searchV2/allowed_actions_test.go b/pkg/services/searchV2/allowed_actions_test.go new file mode 100644 index 00000000000..ce0ced9f5af --- /dev/null +++ b/pkg/services/searchV2/allowed_actions_test.go @@ -0,0 +1,119 @@ +package searchV2 + +import ( + "context" + _ "embed" + "fmt" + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana-plugin-sdk-go/experimental" + "github.com/grafana/grafana/pkg/models" + ac "github.com/grafana/grafana/pkg/services/accesscontrol" + accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/stretchr/testify/require" +) + +var ( + //go:embed testdata/search_response_frame.json + exampleListFrameJSON string + + orgId = int64(1) + permissionsWithScopeAll = map[string][]string{ + datasources.ActionIDRead: {datasources.ScopeAll}, + datasources.ActionDelete: {datasources.ScopeAll}, + ac.ActionDatasourcesExplore: {datasources.ScopeAll}, + datasources.ActionQuery: {datasources.ScopeAll}, + datasources.ActionRead: {datasources.ScopeAll}, + datasources.ActionWrite: {datasources.ScopeAll}, + datasources.ActionPermissionsRead: {datasources.ScopeAll}, + datasources.ActionPermissionsWrite: {datasources.ScopeAll}, + + dashboards.ActionFoldersCreate: {dashboards.ScopeFoldersAll}, + dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}, + dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}, + dashboards.ActionFoldersDelete: {dashboards.ScopeFoldersAll}, + dashboards.ActionFoldersPermissionsRead: {dashboards.ScopeFoldersAll}, + dashboards.ActionFoldersPermissionsWrite: {dashboards.ScopeFoldersAll}, + + dashboards.ActionDashboardsCreate: {dashboards.ScopeDashboardsAll}, + dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsAll}, + dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsAll}, + dashboards.ActionDashboardsDelete: {dashboards.ScopeDashboardsAll}, + dashboards.ActionDashboardsPermissionsRead: {dashboards.ScopeDashboardsAll}, + dashboards.ActionDashboardsPermissionsWrite: {dashboards.ScopeDashboardsAll}, + } + permissionsWithUidScopes = map[string][]string{ + datasources.ActionIDRead: {}, + datasources.ActionDelete: {}, + ac.ActionDatasourcesExplore: {}, + datasources.ActionQuery: {}, + datasources.ActionRead: { + datasources.ScopeProvider.GetResourceScopeUID("datasource-2"), + datasources.ScopeProvider.GetResourceScopeUID("datasource-3"), + }, + datasources.ActionWrite: {}, + datasources.ActionPermissionsRead: {}, + datasources.ActionPermissionsWrite: {}, + + dashboards.ActionFoldersCreate: {}, + dashboards.ActionFoldersRead: { + dashboards.ScopeFoldersProvider.GetResourceScopeUID("ujaM1h6nz"), + }, + dashboards.ActionFoldersWrite: {}, + dashboards.ActionFoldersDelete: {}, + dashboards.ActionFoldersPermissionsRead: {}, + dashboards.ActionFoldersPermissionsWrite: {}, + + dashboards.ActionDashboardsCreate: {}, + dashboards.ActionDashboardsRead: {}, + dashboards.ActionDashboardsWrite: { + dashboards.ScopeDashboardsProvider.GetResourceScopeUID("7MeksYbmk"), + }, + dashboards.ActionDashboardsDelete: {}, + dashboards.ActionDashboardsPermissionsRead: {}, + dashboards.ActionDashboardsPermissionsWrite: {}, + } +) + +func service(t *testing.T) *StandardSearchService { + service, ok := ProvideService(nil, nil, nil, accesscontrolmock.New()).(*StandardSearchService) + require.True(t, ok) + return service +} + +func TestAllowedActionsForPermissionsWithScopeAll(t *testing.T) { + tests := []struct { + name string + permissions map[string][]string + }{ + { + name: "scope_all", + permissions: permissionsWithScopeAll, + }, + { + name: "scope_uids", + permissions: permissionsWithUidScopes, + }, + } + + for _, tt := range tests { + frame := &data.Frame{} + err := frame.UnmarshalJSON([]byte(exampleListFrameJSON)) + require.NoError(t, err) + + err = service(t).addAllowedActionsField(context.Background(), orgId, &models.SignedInUser{ + Permissions: map[int64]map[string][]string{ + orgId: tt.permissions, + }, + }, &backend.DataResponse{ + Frames: []*data.Frame{frame}, + }) + require.NoError(t, err) + + experimental.CheckGoldenJSONFrame(t, "testdata", fmt.Sprintf("allowed_actions_%s.golden", tt.name), frame, true) + } +} diff --git a/pkg/services/searchV2/filter.go b/pkg/services/searchV2/filter.go index 354e7fdc499..53f4984bed8 100644 --- a/pkg/services/searchV2/filter.go +++ b/pkg/services/searchV2/filter.go @@ -18,9 +18,10 @@ type PermissionFilter struct { type entityKind string const ( - entityKindPanel entityKind = "panel" - entityKindDashboard entityKind = "dashboard" - entityKindFolder entityKind = "folder" + entityKindPanel entityKind = "panel" + entityKindDashboard entityKind = "dashboard" + entityKindFolder entityKind = "folder" + entityKindDatasource entityKind = "datasource" ) func (r entityKind) IsValid() bool { diff --git a/pkg/services/searchV2/service.go b/pkg/services/searchV2/service.go index 2bd11f15b60..8f4f29c545f 100644 --- a/pkg/services/searchV2/service.go +++ b/pkg/services/searchV2/service.go @@ -175,5 +175,13 @@ func (s *StandardSearchService) DoDashboardQuery(ctx context.Context, user *back return rsp } - return doSearchQuery(ctx, s.logger, index, filter, q, s.extender.GetQueryExtender(q), s.cfg.AppSubURL) + response := doSearchQuery(ctx, s.logger, index, filter, q, s.extender.GetQueryExtender(q), s.cfg.AppSubURL) + + if q.WithAllowedActions { + if err := s.addAllowedActionsField(ctx, orgID, signedInUser, response); err != nil { + s.logger.Error("error when adding the allowedActions field", "err", err) + } + } + + return response } diff --git a/pkg/services/searchV2/testdata/allowed_actions_scope_all.golden.jsonc b/pkg/services/searchV2/testdata/allowed_actions_scope_all.golden.jsonc new file mode 100644 index 00000000000..3e65fd18bfc --- /dev/null +++ b/pkg/services/searchV2/testdata/allowed_actions_scope_all.golden.jsonc @@ -0,0 +1,321 @@ +// 🌟 This was machine generated. Do not edit. 🌟 +// +// Frame[0] { +// "type": "search-results", +// "custom": { +// "count": 106, +// "locationInfo": { +// "yboVMzb7z": { +// "kind": "folder", +// "name": "gdev dashboards", +// "url": "/dashboards/f/yboVMzb7z/gdev-dashboards" +// } +// }, +// "sortBy": "name_sort" +// } +// } +// Name: Query results +// Dimensions: 9 Fields by 4 Rows +// +----------------+----------------+-------------------------+------------------+--------------------------------------+--------------------------+---------------------------+----------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location | Name: allowed_actions | +// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | +// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string | Type: []json.RawMessage | +// +----------------+----------------+-------------------------+------------------+--------------------------------------+--------------------------+---------------------------+----------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +// | folder | ujaM1h6nz | abc2 | | /dashboards/f/ujaM1h6nz/abc2 | null | null | | [{"kind":"folder","uid":"ujaM1h6nz","actions":["folders.permissions:read","folders.permissions:write","folders:create","folders:delete","folders:read","folders:write"]}] | +// | dashboard | 7MeksYbmk | Alerting with TestData | | /d/7MeksYbmk/alerting-with-testdata | [ | [ | yboVMzb7z | [{"kind":"dashboard","uid":"7MeksYbmk","actions":["dashboards.permissions:read","dashboards.permissions:write","dashboards:create","dashboards:delete","dashboards:read","dashboards:write"]},{"kind":"datasource","uid":"datasource-1","actions":["datasources.id:read","datasources.permissions:read","datasources.permissions:write","datasources:delete","datasources:explore","datasources:query","datasources:read","datasources:write"]}] | +// | | | | | | "gdev", | "datasource-1" | | | +// | | | | | | "alerting" | ] | | | +// | | | | | | ] | | | | +// | dashboard | vmie2cmWz | Bar Gauge Demo | | /d/vmie2cmWz/bar-gauge-demo | [ | [ | yboVMzb7z | [{"kind":"dashboard","uid":"vmie2cmWz","actions":["dashboards.permissions:read","dashboards.permissions:write","dashboards:create","dashboards:delete","dashboards:read","dashboards:write"]},{"kind":"datasource","uid":"datasource-2","actions":["datasources.id:read","datasources.permissions:read","datasources.permissions:write","datasources:delete","datasources:explore","datasources:query","datasources:read","datasources:write"]},{"kind":"datasource","uid":"datasource-3","actions":["datasources.id:read","datasources.permissions:read","datasources.permissions:write","datasources:delete","datasources:explore","datasources:query","datasources:read","datasources:write"]},{"kind":"datasource","uid":"datasource-4","actions":["datasources.id:read","datasources.permissions:read","datasources.permissions:write","datasources:delete","datasources:explore","datasources:query","datasources:read","datasources:write"]}] | +// | | | | | | "gdev", | "datasource-2", | | | +// | | | | | | "demo" | "datasource-3", | | | +// | | | | | | ] | "datasource-4" | | | +// | | | | | | | ] | | | +// | dashboard | xMsQdBfWz | Bar Gauge Demo Unfilled | | /d/xMsQdBfWz/bar-gauge-demo-unfilled | [ | [] | yboVMzb7z | [{"kind":"dashboard","uid":"xMsQdBfWz","actions":["dashboards.permissions:read","dashboards.permissions:write","dashboards:create","dashboards:delete","dashboards:read","dashboards:write"]}] | +// | | | | | | "gdev", | | | | +// | | | | | | "demo" | | | | +// | | | | | | ] | | | | +// +----------------+----------------+-------------------------+------------------+--------------------------------------+--------------------------+---------------------------+----------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +// +// +// 🌟 This was machine generated. Do not edit. 🌟 +{ + "frames": [ + { + "schema": { + "name": "Query results", + "refId": "Search", + "meta": { + "type": "search-results", + "custom": { + "count": 106, + "locationInfo": { + "yboVMzb7z": { + "kind": "folder", + "name": "gdev dashboards", + "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": true + } + }, + { + "name": "location", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "allowed_actions", + "type": "other", + "typeInfo": { + "frame": "json.RawMessage" + } + } + ] + }, + "data": { + "values": [ + [ + "folder", + "dashboard", + "dashboard", + "dashboard" + ], + [ + "ujaM1h6nz", + "7MeksYbmk", + "vmie2cmWz", + "xMsQdBfWz" + ], + [ + "abc2", + "Alerting with TestData", + "Bar Gauge Demo", + "Bar Gauge Demo Unfilled" + ], + [ + "", + "", + "", + "" + ], + [ + "/dashboards/f/ujaM1h6nz/abc2", + "/d/7MeksYbmk/alerting-with-testdata", + "/d/vmie2cmWz/bar-gauge-demo", + "/d/xMsQdBfWz/bar-gauge-demo-unfilled" + ], + [ + null, + [ + "gdev", + "alerting" + ], + [ + "gdev", + "demo" + ], + [ + "gdev", + "demo" + ] + ], + [ + null, + [ + "datasource-1" + ], + [ + "datasource-2", + "datasource-3", + "datasource-4" + ], + [] + ], + [ + "", + "yboVMzb7z", + "yboVMzb7z", + "yboVMzb7z" + ], + [ + [ + { + "kind": "folder", + "uid": "ujaM1h6nz", + "actions": [ + "folders.permissions:read", + "folders.permissions:write", + "folders:create", + "folders:delete", + "folders:read", + "folders:write" + ] + } + ], + [ + { + "kind": "dashboard", + "uid": "7MeksYbmk", + "actions": [ + "dashboards.permissions:read", + "dashboards.permissions:write", + "dashboards:create", + "dashboards:delete", + "dashboards:read", + "dashboards:write" + ] + }, + { + "kind": "datasource", + "uid": "datasource-1", + "actions": [ + "datasources.id:read", + "datasources.permissions:read", + "datasources.permissions:write", + "datasources:delete", + "datasources:explore", + "datasources:query", + "datasources:read", + "datasources:write" + ] + } + ], + [ + { + "kind": "dashboard", + "uid": "vmie2cmWz", + "actions": [ + "dashboards.permissions:read", + "dashboards.permissions:write", + "dashboards:create", + "dashboards:delete", + "dashboards:read", + "dashboards:write" + ] + }, + { + "kind": "datasource", + "uid": "datasource-2", + "actions": [ + "datasources.id:read", + "datasources.permissions:read", + "datasources.permissions:write", + "datasources:delete", + "datasources:explore", + "datasources:query", + "datasources:read", + "datasources:write" + ] + }, + { + "kind": "datasource", + "uid": "datasource-3", + "actions": [ + "datasources.id:read", + "datasources.permissions:read", + "datasources.permissions:write", + "datasources:delete", + "datasources:explore", + "datasources:query", + "datasources:read", + "datasources:write" + ] + }, + { + "kind": "datasource", + "uid": "datasource-4", + "actions": [ + "datasources.id:read", + "datasources.permissions:read", + "datasources.permissions:write", + "datasources:delete", + "datasources:explore", + "datasources:query", + "datasources:read", + "datasources:write" + ] + } + ], + [ + { + "kind": "dashboard", + "uid": "xMsQdBfWz", + "actions": [ + "dashboards.permissions:read", + "dashboards.permissions:write", + "dashboards:create", + "dashboards:delete", + "dashboards:read", + "dashboards:write" + ] + } + ] + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/services/searchV2/testdata/allowed_actions_scope_uids.golden.jsonc b/pkg/services/searchV2/testdata/allowed_actions_scope_uids.golden.jsonc new file mode 100644 index 00000000000..d79fca3b0c0 --- /dev/null +++ b/pkg/services/searchV2/testdata/allowed_actions_scope_uids.golden.jsonc @@ -0,0 +1,265 @@ +// 🌟 This was machine generated. Do not edit. 🌟 +// +// Frame[0] { +// "type": "search-results", +// "custom": { +// "count": 106, +// "locationInfo": { +// "yboVMzb7z": { +// "kind": "folder", +// "name": "gdev dashboards", +// "url": "/dashboards/f/yboVMzb7z/gdev-dashboards" +// } +// }, +// "sortBy": "name_sort" +// } +// } +// Name: Query results +// Dimensions: 9 Fields by 4 Rows +// +----------------+----------------+-------------------------+------------------+--------------------------------------+--------------------------+---------------------------+----------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +// | Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location | Name: allowed_actions | +// | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | +// | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string | Type: []json.RawMessage | +// +----------------+----------------+-------------------------+------------------+--------------------------------------+--------------------------+---------------------------+----------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +// | folder | ujaM1h6nz | abc2 | | /dashboards/f/ujaM1h6nz/abc2 | null | null | | [{"kind":"folder","uid":"ujaM1h6nz","actions":["folders:read"]}] | +// | dashboard | 7MeksYbmk | Alerting with TestData | | /d/7MeksYbmk/alerting-with-testdata | [ | [ | yboVMzb7z | [{"kind":"dashboard","uid":"7MeksYbmk","actions":["dashboards:write"]},{"kind":"datasource","uid":"datasource-1","actions":[]}] | +// | | | | | | "gdev", | "datasource-1" | | | +// | | | | | | "alerting" | ] | | | +// | | | | | | ] | | | | +// | dashboard | vmie2cmWz | Bar Gauge Demo | | /d/vmie2cmWz/bar-gauge-demo | [ | [ | yboVMzb7z | [{"kind":"dashboard","uid":"vmie2cmWz","actions":[]},{"kind":"datasource","uid":"datasource-2","actions":["datasources:read"]},{"kind":"datasource","uid":"datasource-3","actions":["datasources:read"]},{"kind":"datasource","uid":"datasource-4","actions":[]}] | +// | | | | | | "gdev", | "datasource-2", | | | +// | | | | | | "demo" | "datasource-3", | | | +// | | | | | | ] | "datasource-4" | | | +// | | | | | | | ] | | | +// | dashboard | xMsQdBfWz | Bar Gauge Demo Unfilled | | /d/xMsQdBfWz/bar-gauge-demo-unfilled | [ | [] | yboVMzb7z | [{"kind":"dashboard","uid":"xMsQdBfWz","actions":[]}] | +// | | | | | | "gdev", | | | | +// | | | | | | "demo" | | | | +// | | | | | | ] | | | | +// +----------------+----------------+-------------------------+------------------+--------------------------------------+--------------------------+---------------------------+----------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +// +// +// 🌟 This was machine generated. Do not edit. 🌟 +{ + "frames": [ + { + "schema": { + "name": "Query results", + "refId": "Search", + "meta": { + "type": "search-results", + "custom": { + "count": 106, + "locationInfo": { + "yboVMzb7z": { + "kind": "folder", + "name": "gdev dashboards", + "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": true + } + }, + { + "name": "location", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "allowed_actions", + "type": "other", + "typeInfo": { + "frame": "json.RawMessage" + } + } + ] + }, + "data": { + "values": [ + [ + "folder", + "dashboard", + "dashboard", + "dashboard" + ], + [ + "ujaM1h6nz", + "7MeksYbmk", + "vmie2cmWz", + "xMsQdBfWz" + ], + [ + "abc2", + "Alerting with TestData", + "Bar Gauge Demo", + "Bar Gauge Demo Unfilled" + ], + [ + "", + "", + "", + "" + ], + [ + "/dashboards/f/ujaM1h6nz/abc2", + "/d/7MeksYbmk/alerting-with-testdata", + "/d/vmie2cmWz/bar-gauge-demo", + "/d/xMsQdBfWz/bar-gauge-demo-unfilled" + ], + [ + null, + [ + "gdev", + "alerting" + ], + [ + "gdev", + "demo" + ], + [ + "gdev", + "demo" + ] + ], + [ + null, + [ + "datasource-1" + ], + [ + "datasource-2", + "datasource-3", + "datasource-4" + ], + [] + ], + [ + "", + "yboVMzb7z", + "yboVMzb7z", + "yboVMzb7z" + ], + [ + [ + { + "kind": "folder", + "uid": "ujaM1h6nz", + "actions": [ + "folders:read" + ] + } + ], + [ + { + "kind": "dashboard", + "uid": "7MeksYbmk", + "actions": [ + "dashboards:write" + ] + }, + { + "kind": "datasource", + "uid": "datasource-1", + "actions": [] + } + ], + [ + { + "kind": "dashboard", + "uid": "vmie2cmWz", + "actions": [] + }, + { + "kind": "datasource", + "uid": "datasource-2", + "actions": [ + "datasources:read" + ] + }, + { + "kind": "datasource", + "uid": "datasource-3", + "actions": [ + "datasources:read" + ] + }, + { + "kind": "datasource", + "uid": "datasource-4", + "actions": [] + } + ], + [ + { + "kind": "dashboard", + "uid": "xMsQdBfWz", + "actions": [] + } + ] + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/services/searchV2/testdata/search_response_frame.json b/pkg/services/searchV2/testdata/search_response_frame.json new file mode 100644 index 00000000000..c7b37d7eafe --- /dev/null +++ b/pkg/services/searchV2/testdata/search_response_frame.json @@ -0,0 +1,155 @@ +{ + "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": true + } + }, + { + "name": "location", + "type": "string", + "typeInfo": { + "frame": "string" + } + } + ] + }, + "data": { + "values": [ + [ + "folder", + "dashboard", + "dashboard", + "dashboard" + ], + [ + "ujaM1h6nz", + "7MeksYbmk", + "vmie2cmWz", + "xMsQdBfWz" + ], + [ + "abc2", + "Alerting with TestData", + "Bar Gauge Demo", + "Bar Gauge Demo Unfilled" + ], + [ + "", + "", + "", + "" + ], + [ + "/dashboards/f/ujaM1h6nz/abc2", + "/d/7MeksYbmk/alerting-with-testdata", + "/d/vmie2cmWz/bar-gauge-demo", + "/d/xMsQdBfWz/bar-gauge-demo-unfilled" + ], + [ + null, + [ + "gdev", + "alerting" + ], + [ + "gdev", + "demo" + ], + [ + "gdev", + "demo" + ] + ], + [ + null, + [ + "datasource-1" + ], + [ + "datasource-2", + "datasource-3", + "datasource-4" + ], + [] + ], + [ + "", + "yboVMzb7z", + "yboVMzb7z", + "yboVMzb7z" + ] + ] + } +} diff --git a/pkg/services/searchV2/types.go b/pkg/services/searchV2/types.go index 17dcbb54840..e180e2ebc5f 100644 --- a/pkg/services/searchV2/types.go +++ b/pkg/services/searchV2/types.go @@ -14,21 +14,21 @@ type FacetField struct { } type DashboardQuery struct { - Query string `json:"query"` - Location string `json:"location,omitempty"` // parent folder ID - Sort string `json:"sort,omitempty"` // field ASC/DESC - Datasource string `json:"ds_uid,omitempty"` // "datasource" collides with the JSON value at the same leel :() - Tags []string `json:"tags,omitempty"` - Kind []string `json:"kind,omitempty"` - PanelType string `json:"panel_type,omitempty"` - UIDs []string `json:"uid,omitempty"` - Explain bool `json:"explain,omitempty"` // adds details on why document matched - Facet []FacetField `json:"facet,omitempty"` - SkipLocation bool `json:"skipLocation,omitempty"` - AccessInfo bool `json:"accessInfo,omitempty"` // adds field for access control - HasPreview string `json:"hasPreview,omitempty"` // the light|dark theme - Limit int `json:"limit,omitempty"` // explicit page size - From int `json:"from,omitempty"` // for paging + Query string `json:"query"` + Location string `json:"location,omitempty"` // parent folder ID + Sort string `json:"sort,omitempty"` // field ASC/DESC + Datasource string `json:"ds_uid,omitempty"` // "datasource" collides with the JSON value at the same leel :() + Tags []string `json:"tags,omitempty"` + Kind []string `json:"kind,omitempty"` + PanelType string `json:"panel_type,omitempty"` + UIDs []string `json:"uid,omitempty"` + Explain bool `json:"explain,omitempty"` // adds details on why document matched + WithAllowedActions bool `json:"withAllowedActions,omitempty"` // adds allowed actions per entity + Facet []FacetField `json:"facet,omitempty"` + SkipLocation bool `json:"skipLocation,omitempty"` + HasPreview string `json:"hasPreview,omitempty"` // the light|dark theme + Limit int `json:"limit,omitempty"` // explicit page size + From int `json:"from,omitempty"` // for paging } type SearchService interface { diff --git a/public/app/features/search/page/components/ExplainScorePopup.tsx b/public/app/features/search/page/components/ExplainScorePopup.tsx index 5f14e7f7947..c31b7159553 100644 --- a/public/app/features/search/page/components/ExplainScorePopup.tsx +++ b/public/app/features/search/page/components/ExplainScorePopup.tsx @@ -14,6 +14,7 @@ export interface Props { const tabs = [ { label: 'Score', value: 'score' }, { label: 'Fields', value: 'fields' }, + { label: 'Allowed actions', value: 'allowed_actions' }, ]; export function ExplainScorePopup({ name, explain, frame, row }: Props) { @@ -51,6 +52,21 @@ export function ExplainScorePopup({ name, explain, frame, row }: Props) { )} + {activeTab === tabs[2].value && ( + { + const allowedActions = frame.fields.find((f) => f.name === 'allowed_actions')?.values?.get(row); + const dsUids = frame.fields.find((f) => f.name === 'ds_uid')?.values?.get(row); + return JSON.stringify({ dsUids: dsUids ?? [], allowedActions: allowedActions ?? [] }, null, 2); + })()} + readOnly={false} + /> + )} ); diff --git a/public/app/features/search/page/components/SearchView.tsx b/public/app/features/search/page/components/SearchView.tsx index adddc05973b..4e406c34809 100644 --- a/public/app/features/search/page/components/SearchView.tsx +++ b/public/app/features/search/page/components/SearchView.tsx @@ -65,6 +65,7 @@ export const SearchView = ({ location: folderDTO?.uid, // This will scope all results to the prefix sort: query.sort?.value, explain: query.explain, + withAllowedActions: query.explain, // allowedActions are currently not used for anything on the UI and added only in `explain` mode }; // Only dashboards have additional properties diff --git a/public/app/features/search/service/types.ts b/public/app/features/search/service/types.ts index adea9ba357d..efbaad3d21a 100644 --- a/public/app/features/search/service/types.ts +++ b/public/app/features/search/service/types.ts @@ -18,6 +18,7 @@ export interface SearchQuery { id?: number[]; facet?: FacetField[]; explain?: boolean; + withAllowedActions?: boolean; accessInfo?: boolean; hasPreview?: string; // theme limit?: number;