diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index b930f2333be..16695404cda 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -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}) } diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index b943c24fd44..d0f8e67f9c4 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -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 diff --git a/pkg/services/folder/folderimpl/sqlstore.go b/pkg/services/folder/folderimpl/sqlstore.go index 7bac08fe0d7..f08ae8d9ede 100644 --- a/pkg/services/folder/folderimpl/sqlstore.go +++ b/pkg/services/folder/folderimpl/sqlstore.go @@ -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 +} diff --git a/pkg/services/folder/folderimpl/sqlstore_test.go b/pkg/services/folder/folderimpl/sqlstore_test.go index 21f7ba88009..e8f61c59d92 100644 --- a/pkg/services/folder/folderimpl/sqlstore_test.go +++ b/pkg/services/folder/folderimpl/sqlstore_test.go @@ -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() diff --git a/pkg/services/folder/folderimpl/store.go b/pkg/services/folder/folderimpl/store.go index a286391a887..b6dca0ef4b5 100644 --- a/pkg/services/folder/folderimpl/store.go +++ b/pkg/services/folder/folderimpl/store.go @@ -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) } diff --git a/pkg/services/folder/folderimpl/store_fake.go b/pkg/services/folder/folderimpl/store_fake.go index 9a8a47a4ae8..7c6a63457cc 100644 --- a/pkg/services/folder/folderimpl/store_fake.go +++ b/pkg/services/folder/folderimpl/store_fake.go @@ -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 +}