mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
5fc3ab801b
commit
a39242890e
@ -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})
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user