mirror of
https://github.com/grafana/grafana.git
synced 2025-02-11 16:15:42 -06:00
AccessControl: Add metadata to search result (#48879)
* Add access control metadata to search hits if access control query string is passed
This commit is contained in:
parent
7cb7290a3e
commit
34be8f28b9
@ -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 {
|
||||
|
77
pkg/api/search_test.go
Normal file
77
pkg/api/search_test.go
Normal file
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user