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;