diff --git a/pkg/api/search.go b/pkg/api/search.go index a981250d4d4..a28d0275a3b 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -4,12 +4,13 @@ import ( "net/http" "strconv" - "github.com/grafana/grafana/pkg/util" - "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/metrics" "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/search" + "github.com/grafana/grafana/pkg/util" ) func (hs *HTTPServer) Search(c *models.ReqContext) response.Response { @@ -66,8 +67,48 @@ func (hs *HTTPServer) Search(c *models.ReqContext) response.Response { return response.Error(500, "Search failed", err) } - c.TimeRequest(metrics.MApiDashboardSearch) - return response.JSON(http.StatusOK, searchQuery.Result) + defer c.TimeRequest(metrics.MApiDashboardSearch) + + if !c.QueryBool("accesscontrol") { + return response.JSON(http.StatusOK, searchQuery.Result) + } + + return hs.searchHitsWithMetadata(c, searchQuery.Result) +} + +func (hs *HTTPServer) searchHitsWithMetadata(c *models.ReqContext, hits models.HitList) response.Response { + folderUIDs := make(map[string]bool) + dashboardUIDs := make(map[string]bool) + + for _, hit := range hits { + if hit.Type == models.DashHitFolder { + folderUIDs[hit.UID] = true + } else { + dashboardUIDs[hit.UID] = true + folderUIDs[hit.FolderUID] = true + } + } + + folderMeta := hs.getMultiAccessControlMetadata(c, c.OrgId, dashboards.ScopeFoldersPrefix, folderUIDs) + dashboardMeta := hs.getMultiAccessControlMetadata(c, c.OrgId, dashboards.ScopeDashboardsPrefix, dashboardUIDs) + + // search hit with access control metadata attached + type hitWithMeta struct { + *models.Hit + AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"` + } + hitsWithMeta := make([]hitWithMeta, 0, len(hits)) + for _, hit := range hits { + var meta accesscontrol.Metadata + if hit.Type == models.DashHitFolder { + meta = folderMeta[hit.UID] + } else { + meta = accesscontrol.MergeMeta("dashboards", dashboardMeta[hit.UID], folderMeta[hit.FolderUID]) + } + hitsWithMeta = append(hitsWithMeta, hitWithMeta{hit, meta}) + } + + return response.JSON(http.StatusOK, hitsWithMeta) } func (hs *HTTPServer) ListSortOptions(c *models.ReqContext) response.Response { diff --git a/pkg/api/search_test.go b/pkg/api/search_test.go new file mode 100644 index 00000000000..8a376d67dda --- /dev/null +++ b/pkg/api/search_test.go @@ -0,0 +1,77 @@ +package api + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/dashboards" +) + +func TestHTTPServer_Search(t *testing.T) { + sc := setupHTTPServer(t, true, true) + sc.initCtx.IsSignedIn = true + sc.initCtx.SignedInUser = &models.SignedInUser{} + + sc.hs.SearchService = &mockSearchService{ + ExpectedResult: models.HitList{ + {ID: 1, UID: "folder1", Title: "folder1", Type: models.DashHitFolder}, + {ID: 2, UID: "folder2", Title: "folder2", Type: models.DashHitFolder}, + {ID: 3, UID: "dash3", Title: "dash3", FolderUID: "folder2", Type: models.DashHitDB}, + }, + } + + sc.acmock.GetUserPermissionsFunc = func(ctx context.Context, user *models.SignedInUser, options accesscontrol.Options) ([]*accesscontrol.Permission, error) { + return []*accesscontrol.Permission{ + {Action: "folders:read", Scope: "folders:*"}, + {Action: "folders:write", Scope: "folders:uid:folder2"}, + {Action: "dashboards:read", Scope: "dashboards:*"}, + {Action: "dashboards:write", Scope: "folders:uid:folder2"}, + }, nil + } + + type withMeta struct { + models.Hit + AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"` + } + + t.Run("should attach access control metadata to response", func(t *testing.T) { + recorder := callAPI(sc.server, http.MethodGet, "/api/search?accesscontrol=true", nil, t) + assert.Equal(t, http.StatusOK, recorder.Code) + var result []withMeta + require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result)) + + for _, r := range result { + if r.ID == 1 { + assert.Len(t, r.AccessControl, 1) + assert.True(t, r.AccessControl[dashboards.ActionFoldersRead]) + } else if r.ID == 2 { + assert.Len(t, r.AccessControl, 3) + assert.True(t, r.AccessControl[dashboards.ActionFoldersRead]) + assert.True(t, r.AccessControl[dashboards.ActionFoldersWrite]) + assert.True(t, r.AccessControl[dashboards.ActionDashboardsWrite]) + } else if r.ID == 3 { + assert.Len(t, r.AccessControl, 2) + assert.True(t, r.AccessControl[dashboards.ActionDashboardsRead]) + assert.True(t, r.AccessControl[dashboards.ActionDashboardsWrite]) + } + } + }) + + t.Run("should not attach access control metadata to response", func(t *testing.T) { + recorder := callAPI(sc.server, http.MethodGet, "/api/search", nil, t) + assert.Equal(t, http.StatusOK, recorder.Code) + var result []withMeta + require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &result)) + + for _, r := range result { + assert.Len(t, r.AccessControl, 0) + } + }) +} diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index de918638f34..4a17b0d6c1d 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -213,6 +213,20 @@ func GetResourcesMetadata(ctx context.Context, permissions map[string][]string, return result } +// MergeMeta will merge actions matching prefix of second metadata into first +func MergeMeta(prefix string, first Metadata, second Metadata) Metadata { + if first == nil { + first = Metadata{} + } + + for key := range second { + if strings.HasPrefix(key, prefix) { + first[key] = true + } + } + return first +} + func ManagedUserRoleName(userID int64) string { return fmt.Sprintf("managed:users:%d:permissions", userID) }