Folders: Able to fetch folders available for user as "shared" folder (#77774)

* Folders: Show folders user has access to at the root level

* Refactor

* Refactor

* Hide parent folders user has no access to

* Skip expensive computation if possible

* Fix tests

* Fix potential nil access

* Fix duplicated folders

* Fix linter error

* Fix querying folders if no managed permissions set

* Update benchmark

* Add special shared with me folder and fetch available non-root folders on demand

* Fix parents query

* Improve db query for folders

* Reset benchmark changes

* Fix permissions for shared with me folder

* Simplify dedup

* Add option to include shared folder permission to user's permissions

* Fix nil UID

* Remove duplicated folders from shared list

* Only left the base part

* Apply suggestions from code review

Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com>

* Add tests

* Fix linter errors

---------

Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com>
This commit is contained in:
Alexander Zobnin 2023-11-08 15:28:49 +01:00 committed by GitHub
parent 5fc3ab801b
commit a39242890e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 173 additions and 1 deletions

View File

@ -312,5 +312,5 @@ func setUpRBACGuardian(t *testing.T) {
guardian.New = origNewGuardian
})
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanEditValue: true})
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanEditValue: true, CanViewValue: true})
}

View File

@ -7,6 +7,8 @@ import (
"strings"
"sync"
"golang.org/x/exp/slices"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/events"
"github.com/grafana/grafana/pkg/infra/db"
@ -246,6 +248,85 @@ func (s *Service) GetChildren(ctx context.Context, cmd *folder.GetChildrenQuery)
return filtered, nil
}
// GetSharedWithMe returns folders available to user, which cannot be accessed from the root folders
func (s *Service) GetSharedWithMe(ctx context.Context, cmd *folder.GetChildrenQuery) ([]*folder.Folder, error) {
availableNonRootFolders, err := s.getAvailableNonRootFolders(ctx, cmd.OrgID, cmd.SignedInUser)
if err != nil {
return nil, folder.ErrInternal.Errorf("failed to fetch subfolders to which the user has explicit access: %w", err)
}
rootFolders, err := s.GetChildren(ctx, &folder.GetChildrenQuery{UID: "", OrgID: cmd.OrgID, SignedInUser: cmd.SignedInUser})
if err != nil {
return nil, folder.ErrInternal.Errorf("failed to fetch root folders to which the user has access: %w", err)
}
availableNonRootFolders = s.deduplicateAvailableFolders(ctx, availableNonRootFolders, rootFolders)
return availableNonRootFolders, nil
}
func (s *Service) getAvailableNonRootFolders(ctx context.Context, orgID int64, user identity.Requester) ([]*folder.Folder, error) {
permissions := user.GetPermissions()
folderPermissions := permissions["folders:read"]
folderPermissions = append(folderPermissions, permissions["dashboards:read"]...)
nonRootFolders := make([]*folder.Folder, 0)
folderUids := make([]string, 0)
for _, p := range folderPermissions {
if folderUid, found := strings.CutPrefix(p, "folders:uid:"); found {
if !slices.Contains(folderUids, folderUid) {
folderUids = append(folderUids, folderUid)
}
}
}
if len(folderUids) == 0 {
return nonRootFolders, nil
}
dashFolders, err := s.store.GetFolders(ctx, orgID, folderUids)
if err != nil {
return nil, folder.ErrInternal.Errorf("failed to fetch subfolders: %w", err)
}
for _, f := range dashFolders {
if f.ParentUID != "" {
nonRootFolders = append(nonRootFolders, f)
}
}
return nonRootFolders, nil
}
func (s *Service) deduplicateAvailableFolders(ctx context.Context, folders []*folder.Folder, rootFolders []*folder.Folder) []*folder.Folder {
allFolders := append(folders, rootFolders...)
foldersDedup := make([]*folder.Folder, 0)
for _, f := range folders {
isSubfolder := slices.ContainsFunc(allFolders, func(folder *folder.Folder) bool {
return f.ParentUID == folder.UID
})
if !isSubfolder {
parents, err := s.GetParents(ctx, folder.GetParentsQuery{UID: f.UID, OrgID: f.OrgID})
if err != nil {
s.log.Error("failed to fetch folder parents", "uid", f.UID, "error", err)
continue
}
for _, parent := range parents {
contains := slices.ContainsFunc(allFolders, func(f *folder.Folder) bool {
return f.UID == parent.UID
})
if contains {
isSubfolder = true
break
}
}
}
if !isSubfolder {
foldersDedup = append(foldersDedup, f)
}
}
return foldersDedup
}
func (s *Service) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
if !s.features.IsEnabled(featuremgmt.FlagNestedFolders) {
return nil, nil

View File

@ -6,6 +6,7 @@ import (
"time"
"github.com/grafana/dskit/concurrency"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@ -325,3 +326,28 @@ func (ss *sqlStore) GetHeight(ctx context.Context, foldrUID string, orgID int64,
}
return height, nil
}
func (ss *sqlStore) GetFolders(ctx context.Context, orgID int64, uids []string) ([]*folder.Folder, error) {
if len(uids) == 0 {
return []*folder.Folder{}, nil
}
var folders []*folder.Folder
if err := ss.db.WithDbSession(ctx, func(sess *db.Session) error {
b := strings.Builder{}
b.WriteString(`SELECT * FROM folder WHERE org_id=? AND uid IN (?` + strings.Repeat(", ?", len(uids)-1) + `)`)
args := []any{orgID}
for _, uid := range uids {
args = append(args, uid)
}
return sess.SQL(b.String(), args...).Find(&folders)
}); err != nil {
return nil, err
}
// Add URLs
for i, f := range folders {
folders[i] = f.WithURL()
}
return folders, nil
}

View File

@ -3,6 +3,7 @@ package folderimpl
import (
"context"
"fmt"
"slices"
"sort"
"testing"
@ -705,6 +706,62 @@ func TestIntegrationGetHeight(t *testing.T) {
})
}
func TestIntegrationGetFolders(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
foldersNum := 10
db := sqlstore.InitTestDB(t)
folderStore := ProvideStore(db, db.Cfg, featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders))
orgID := CreateOrg(t, db)
// create folders
uids := make([]string, 0)
folders := make([]*folder.Folder, 0)
for i := 0; i < foldersNum; i++ {
uid := util.GenerateShortUID()
f, err := folderStore.Create(context.Background(), folder.CreateFolderCommand{
Title: folderTitle,
Description: folderDsc,
OrgID: orgID,
UID: uid,
})
require.NoError(t, err)
uids = append(uids, uid)
folders = append(folders, f)
}
t.Cleanup(func() {
for _, uid := range uids {
err := folderStore.Delete(context.Background(), uid, orgID)
require.NoError(t, err)
}
})
t.Run("get folders by UIDs should succeed", func(t *testing.T) {
ff, err := folderStore.GetFolders(context.Background(), orgID, uids)
require.NoError(t, err)
assert.Equal(t, len(uids), len(ff))
for _, f := range folders {
folderInResponseIdx := slices.IndexFunc(ff, func(rf *folder.Folder) bool {
return rf.UID == f.UID
})
assert.NotEqual(t, -1, folderInResponseIdx)
rf := ff[folderInResponseIdx]
assert.Equal(t, f.ID, rf.ID)
assert.Equal(t, f.OrgID, rf.OrgID)
assert.Equal(t, f.Title, rf.Title)
assert.Equal(t, f.Description, rf.Description)
assert.NotEmpty(t, rf.Created)
assert.NotEmpty(t, rf.Updated)
assert.NotEmpty(t, rf.URL)
}
})
}
func CreateOrg(t *testing.T, db *sqlstore.SQLStore) int64 {
t.Helper()

View File

@ -33,4 +33,7 @@ type store interface {
// GetHeight returns the height of the folder tree. When parentUID is set, the function would
// verify in the meanwhile that parentUID is not present in the subtree of the folder with the given UID.
GetHeight(ctx context.Context, foldrUID string, orgID int64, parentUID *string) (int, error)
// GetFolders returns folders with given uids
GetFolders(ctx context.Context, orgID int64, uids []string) ([]*folder.Folder, error)
}

View File

@ -9,6 +9,7 @@ import (
type fakeStore struct {
ExpectedChildFolders []*folder.Folder
ExpectedParentFolders []*folder.Folder
ExpectedFolders []*folder.Folder
ExpectedFolder *folder.Folder
ExpectedError error
ExpectedFolderHeight int
@ -55,3 +56,7 @@ func (f *fakeStore) GetChildren(ctx context.Context, cmd folder.GetChildrenQuery
func (f *fakeStore) GetHeight(ctx context.Context, folderUID string, orgID int64, parentUID *string) (int, error) {
return f.ExpectedFolderHeight, f.ExpectedError
}
func (f *fakeStore) GetFolders(ctx context.Context, orgID int64, uids []string) ([]*folder.Folder, error) {
return f.ExpectedFolders, f.ExpectedError
}