diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index bbc032e7382..3bc9d1780bc 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -304,6 +304,32 @@ const rootFolder = "general" var errEmptyResults = fmt.Errorf("empty results") +func permissionToActions(p dashboardaccess.PermissionType) (dashboardAction string, folderAction string) { + switch p { + case dashboardaccess.PERMISSION_EDIT: + return dashboards.ActionDashboardsWrite, dashboards.ActionFoldersWrite + case dashboardaccess.PERMISSION_ADMIN: + return dashboards.ActionDashboardsPermissionsWrite, dashboards.ActionFoldersPermissionsWrite + case dashboardaccess.PERMISSION_VIEW: + fallthrough + default: + return dashboards.ActionDashboardsRead, dashboards.ActionFoldersRead + } +} + +func permissionFromQueryParams(queryParams url.Values) dashboardaccess.PermissionType { + switch strings.ToLower(queryParams.Get("permission")) { + case "edit": + return dashboardaccess.PERMISSION_EDIT + case "view": + return dashboardaccess.PERMISSION_VIEW + case "admin": + return dashboardaccess.PERMISSION_ADMIN + default: + return dashboardaccess.PERMISSION_VIEW + } +} + func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { ctx, span := s.tracer.Start(r.Context(), "dashboard.search") defer span.End() @@ -320,8 +346,10 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { return } + requestedPermission := permissionFromQueryParams(queryParams) + searchRequest, err := convertHttpSearchRequestToResourceSearchRequest(queryParams, user, func() ([]string, error) { - return s.getDashboardsUIDsSharedWithUser(ctx, user) + return s.getDashboardsUIDsSharedWithUser(ctx, user, requestedPermission) }) if err != nil { if errors.Is(err, errEmptyResults) { @@ -548,11 +576,13 @@ func asResourceKey(ns string, k string) (*resourcepb.ResourceKey, error) { return key, nil } -func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, user identity.Requester) ([]string, error) { - // gets dashboards that the user was granted read access to +func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, user identity.Requester, requestedPermission dashboardaccess.PermissionType) ([]string, error) { + // Gets dashboards/folders that the user was granted access to, but does not have access to their parent folder. + // This powers the virtual "Shared with me" folder. + dashboardAction, folderAction := permissionToActions(requestedPermission) permissions := user.GetPermissions() - dashboardPermissions := permissions[dashboards.ActionDashboardsRead] - folderPermissions := permissions[dashboards.ActionFoldersRead] + dashboardPermissions := permissions[dashboardAction] + folderPermissions := permissions[folderAction] dashboardUids := make([]string, 0) sharedDashboards := make([]string, 0) @@ -586,9 +616,10 @@ func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, use } dashboardSearchRequest := &resourcepb.ResourceSearchRequest{ - Federated: []*resourcepb.ResourceKey{folderKey}, - Fields: []string{"folder"}, - Limit: int64(len(dashboardUids)), + Federated: []*resourcepb.ResourceKey{folderKey}, + Fields: []string{"folder"}, + Limit: int64(len(dashboardUids)), + Permission: int64(requestedPermission), Options: &resourcepb.ListOptions{ Key: key, Fields: []*resourcepb.Requirement{{ @@ -625,8 +656,9 @@ func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, use } folderSearchRequest := &resourcepb.ResourceSearchRequest{ - Fields: []string{"folder"}, - Limit: int64(len(allFolders)), + Fields: []string{"folder"}, + Limit: int64(len(allFolders)), + Permission: int64(requestedPermission), Options: &resourcepb.ListOptions{ Key: folderKey, Fields: []*resourcepb.Requirement{{ diff --git a/pkg/registry/apis/dashboard/search_test.go b/pkg/registry/apis/dashboard/search_test.go index c7defba3fea..d45873f9c6c 100644 --- a/pkg/registry/apis/dashboard/search_test.go +++ b/pkg/registry/apis/dashboard/search_test.go @@ -626,6 +626,134 @@ func TestSearchHandlerSharedDashboards(t *testing.T) { require.NoError(t, err) assert.Equal(t, len(mockResponse3.Results.Rows), len(p.Hits)) }) + + t.Run("should compute shared dashboards based on requested edit permission", func(t *testing.T) { + // dashboardSearchRequest + mockResponse1 := &resourcepb.ResourceSearchResponse{ + Results: &resourcepb.ResourceTable{ + Columns: []*resourcepb.ResourceTableColumnDefinition{ + { + Name: "folder", + }, + }, + Rows: []*resourcepb.ResourceTableRow{ + { + Key: &resourcepb.ResourceKey{ + Name: "dashboardinprivatefolder", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("privatefolder"), // folder uid + }, + }, + { + Key: &resourcepb.ResourceKey{ + Name: "dashboardinpublicfolder", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("publicfolder"), // folder uid + }, + }, + }, + }, + } + + // folderSearchRequest: only folders the user can EDIT should be returned here + mockResponse2 := &resourcepb.ResourceSearchResponse{ + Results: &resourcepb.ResourceTable{ + Columns: []*resourcepb.ResourceTableColumnDefinition{ + { + Name: "folder", + }, + }, + Rows: []*resourcepb.ResourceTableRow{ + { + Key: &resourcepb.ResourceKey{ + Name: "publicfolder", + Resource: "folder", + }, + Cells: [][]byte{ + []byte(""), // root folder uid + }, + }, + }, + }, + } + + // final search should only include items in folders the user cannot edit + mockResponse3 := &resourcepb.ResourceSearchResponse{ + Results: &resourcepb.ResourceTable{ + Columns: []*resourcepb.ResourceTableColumnDefinition{ + { + Name: "folder", + }, + }, + Rows: []*resourcepb.ResourceTableRow{ + { + Key: &resourcepb.ResourceKey{ + Name: "dashboardinprivatefolder", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("privatefolder"), // folder uid + }, + }, + }, + }, + } + + mockClient := &MockClient{ + MockResponses: []*resourcepb.ResourceSearchResponse{mockResponse1, mockResponse2, mockResponse3}, + } + + features := featuremgmt.WithFeatures() + searchHandler := SearchHandler{ + log: log.New("test", "test"), + client: mockClient, + tracer: tracing.NewNoopTracerService(), + features: features, + } + rr := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/search?folder=sharedwithme&permission=edit", nil) + req.Header.Add("content-type", "application/json") + + allPermissions := make(map[int64]map[string][]string) + permissions := make(map[string][]string) + permissions[dashboards.ActionDashboardsWrite] = []string{"dashboards:uid:dashboardinprivatefolder", "dashboards:uid:dashboardinpublicfolder"} + // user can view the private folder, but cannot edit it. This should still classify dashboards in it as "shared with me" for edit. + permissions[dashboards.ActionFoldersRead] = []string{"folders:uid:privatefolder"} + allPermissions[1] = permissions + req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test", OrgID: 1, Permissions: allPermissions})) + + searchHandler.DoSearch(rr, req) + + assert.Equal(t, 3, mockClient.CallCount) + + firstCall := mockClient.MockCalls[0] + assert.Equal(t, int64(dashboardaccess.PERMISSION_EDIT), firstCall.Permission) + assert.Equal(t, []string{"dashboardinprivatefolder", "dashboardinpublicfolder"}, firstCall.Options.Fields[0].Values) + + secondCall := mockClient.MockCalls[1] + assert.Equal(t, int64(dashboardaccess.PERMISSION_EDIT), secondCall.Permission) + assert.Equal(t, []string{"privatefolder", "publicfolder"}, secondCall.Options.Fields[0].Values) + + thirdCall := mockClient.MockCalls[2] + assert.Equal(t, int64(dashboardaccess.PERMISSION_EDIT), thirdCall.Permission) + assert.Equal(t, []string{"dashboardinprivatefolder"}, thirdCall.Options.Fields[0].Values) + + resp := rr.Result() + defer func() { + if err := resp.Body.Close(); err != nil { + t.Fatal(err) + } + }() + + p := &v0alpha1.SearchResults{} + err := json.NewDecoder(resp.Body).Decode(p) + require.NoError(t, err) + assert.Equal(t, len(mockResponse3.Results.Rows), len(p.Hits)) + }) } func TestConvertHttpSearchRequestToResourceSearchRequest(t *testing.T) {