Folders: Allow listing folders with write permission (#83527)
* Folders: Allow listing folders with write permission * Check for subfolder access if parent does not have * Add test * GetFolders: fix ordering * Apply suggestion from code review
This commit is contained in:
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
@@ -298,12 +299,16 @@ func (s *Service) GetChildren(ctx context.Context, q *folder.GetChildrenQuery) (
|
||||
return nil, err
|
||||
}
|
||||
|
||||
canView, err := g.CanView()
|
||||
guardianFunc := g.CanView
|
||||
if q.Permission == dashboardaccess.PERMISSION_EDIT {
|
||||
guardianFunc = g.CanEdit
|
||||
}
|
||||
|
||||
hasAccess, err := guardianFunc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !canView {
|
||||
if !hasAccess {
|
||||
return nil, dashboards.ErrFolderAccessDenied
|
||||
}
|
||||
|
||||
@@ -341,8 +346,19 @@ func (s *Service) GetChildren(ctx context.Context, q *folder.GetChildrenQuery) (
|
||||
|
||||
func (s *Service) getRootFolders(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.Folder, error) {
|
||||
permissions := q.SignedInUser.GetPermissions()
|
||||
folderPermissions := permissions[dashboards.ActionFoldersRead]
|
||||
folderPermissions = append(folderPermissions, permissions[dashboards.ActionDashboardsRead]...)
|
||||
var folderPermissions []string
|
||||
if q.Permission == dashboardaccess.PERMISSION_EDIT {
|
||||
folderPermissions = permissions[dashboards.ActionFoldersWrite]
|
||||
folderPermissions = append(folderPermissions, permissions[dashboards.ActionDashboardsWrite]...)
|
||||
} else {
|
||||
folderPermissions = permissions[dashboards.ActionFoldersRead]
|
||||
folderPermissions = append(folderPermissions, permissions[dashboards.ActionDashboardsRead]...)
|
||||
}
|
||||
|
||||
if len(folderPermissions) == 0 && !q.SignedInUser.GetIsGrafanaAdmin() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
q.FolderUIDs = make([]string, 0, len(folderPermissions))
|
||||
for _, p := range folderPermissions {
|
||||
if p == dashboards.ScopeFoldersAll {
|
||||
@@ -401,12 +417,12 @@ func (s *Service) getRootFolders(ctx context.Context, q *folder.GetChildrenQuery
|
||||
// GetSharedWithMe returns folders available to user, which cannot be accessed from the root folders
|
||||
func (s *Service) GetSharedWithMe(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.Folder, error) {
|
||||
start := time.Now()
|
||||
availableNonRootFolders, err := s.getAvailableNonRootFolders(ctx, q.OrgID, q.SignedInUser)
|
||||
availableNonRootFolders, err := s.getAvailableNonRootFolders(ctx, q)
|
||||
if err != nil {
|
||||
s.metrics.sharedWithMeFetchFoldersRequestsDuration.WithLabelValues("failure").Observe(time.Since(start).Seconds())
|
||||
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: q.OrgID, SignedInUser: q.SignedInUser})
|
||||
rootFolders, err := s.GetChildren(ctx, &folder.GetChildrenQuery{UID: "", OrgID: q.OrgID, SignedInUser: q.SignedInUser, Permission: q.Permission})
|
||||
if err != nil {
|
||||
s.metrics.sharedWithMeFetchFoldersRequestsDuration.WithLabelValues("failure").Observe(time.Since(start).Seconds())
|
||||
return nil, folder.ErrInternal.Errorf("failed to fetch root folders to which the user has access: %w", err)
|
||||
@@ -417,10 +433,21 @@ func (s *Service) GetSharedWithMe(ctx context.Context, q *folder.GetChildrenQuer
|
||||
return availableNonRootFolders, nil
|
||||
}
|
||||
|
||||
func (s *Service) getAvailableNonRootFolders(ctx context.Context, orgID int64, user identity.Requester) ([]*folder.Folder, error) {
|
||||
permissions := user.GetPermissions()
|
||||
folderPermissions := permissions[dashboards.ActionFoldersRead]
|
||||
folderPermissions = append(folderPermissions, permissions[dashboards.ActionDashboardsRead]...)
|
||||
func (s *Service) getAvailableNonRootFolders(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.Folder, error) {
|
||||
permissions := q.SignedInUser.GetPermissions()
|
||||
var folderPermissions []string
|
||||
if q.Permission == dashboardaccess.PERMISSION_EDIT {
|
||||
folderPermissions = permissions[dashboards.ActionFoldersWrite]
|
||||
folderPermissions = append(folderPermissions, permissions[dashboards.ActionDashboardsWrite]...)
|
||||
} else {
|
||||
folderPermissions = permissions[dashboards.ActionFoldersRead]
|
||||
folderPermissions = append(folderPermissions, permissions[dashboards.ActionDashboardsRead]...)
|
||||
}
|
||||
|
||||
if len(folderPermissions) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
nonRootFolders := make([]*folder.Folder, 0)
|
||||
folderUids := make([]string, 0, len(folderPermissions))
|
||||
for _, p := range folderPermissions {
|
||||
@@ -437,8 +464,9 @@ func (s *Service) getAvailableNonRootFolders(ctx context.Context, orgID int64, u
|
||||
|
||||
dashFolders, err := s.GetFolders(ctx, folder.GetFoldersQuery{
|
||||
UIDs: folderUids,
|
||||
OrgID: orgID,
|
||||
SignedInUser: user,
|
||||
OrgID: q.OrgID,
|
||||
SignedInUser: q.SignedInUser,
|
||||
OrderByTitle: true,
|
||||
WithFullpathUIDs: true,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -1798,6 +1800,247 @@ func TestFolderServiceGetFolders(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TODO replace it with an API test under /pkg/tests/api/folders
|
||||
// whenever the golang client with get updated to allow filtering child folders by permission
|
||||
func TestGetChildrenFilterByPermission(t *testing.T) {
|
||||
db := sqlstore.InitTestDB(t)
|
||||
|
||||
signedInAdminUser := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{
|
||||
orgID: {
|
||||
dashboards.ActionFoldersCreate: {},
|
||||
dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll},
|
||||
dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll},
|
||||
},
|
||||
}}
|
||||
|
||||
quotaService := quotatest.New(false, nil)
|
||||
folderStore := ProvideDashboardFolderStore(db)
|
||||
|
||||
cfg := setting.NewCfg()
|
||||
|
||||
featuresFlagOff := featuremgmt.WithFeatures()
|
||||
dashStore, err := database.ProvideDashboardStore(db, db.Cfg, featuresFlagOff, tagimpl.ProvideService(db), quotaService)
|
||||
require.NoError(t, err)
|
||||
nestedFolderStore := ProvideStore(db, db.Cfg)
|
||||
|
||||
b := bus.ProvideBus(tracing.InitializeTracerForTest())
|
||||
ac := acimpl.ProvideAccessControl(cfg)
|
||||
|
||||
features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
|
||||
|
||||
folderSvcOn := &Service{
|
||||
cfg: cfg,
|
||||
log: log.New("test-folder-service"),
|
||||
dashboardStore: dashStore,
|
||||
dashboardFolderStore: folderStore,
|
||||
store: nestedFolderStore,
|
||||
features: features,
|
||||
bus: b,
|
||||
db: db,
|
||||
accessControl: ac,
|
||||
registry: make(map[string]folder.RegistryService),
|
||||
metrics: newFoldersMetrics(nil),
|
||||
}
|
||||
|
||||
origGuardian := guardian.New
|
||||
fakeGuardian := &guardian.FakeDashboardGuardian{
|
||||
CanSaveValue: true,
|
||||
CanEditUIDs: []string{},
|
||||
CanViewUIDs: []string{},
|
||||
}
|
||||
guardian.MockDashboardGuardian(fakeGuardian)
|
||||
t.Cleanup(func() {
|
||||
guardian.New = origGuardian
|
||||
})
|
||||
|
||||
viewer := user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{
|
||||
orgID: {
|
||||
dashboards.ActionFoldersRead: {},
|
||||
dashboards.ActionFoldersWrite: {},
|
||||
},
|
||||
}}
|
||||
|
||||
// no view permission
|
||||
// |_ subfolder under no view permission with view permission
|
||||
// |_ subfolder under no view permission with view permissionn and with edit permission
|
||||
// with edit permission
|
||||
// |_ subfolder under with edit permission
|
||||
// no edit permission
|
||||
// |_ subfolder under no edit permission
|
||||
// |_ subfolder under no edit permission with edit permission
|
||||
noViewPermission, err := folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{
|
||||
OrgID: orgID,
|
||||
ParentUID: "",
|
||||
Title: "no view permission",
|
||||
SignedInUser: &signedInAdminUser,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
f, err := folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{
|
||||
OrgID: orgID,
|
||||
ParentUID: noViewPermission.UID,
|
||||
Title: "subfolder under no view permission with view permission",
|
||||
SignedInUser: &signedInAdminUser,
|
||||
})
|
||||
viewer.Permissions[orgID][dashboards.ActionFoldersRead] = append(viewer.Permissions[orgID][dashboards.ActionFoldersRead], dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID))
|
||||
fakeGuardian.CanViewUIDs = append(fakeGuardian.CanViewUIDs, f.UID)
|
||||
|
||||
require.NoError(t, err)
|
||||
f, err = folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{
|
||||
OrgID: orgID,
|
||||
ParentUID: noViewPermission.UID,
|
||||
Title: "subfolder under no view permission with view permission and with edit permission",
|
||||
SignedInUser: &signedInAdminUser,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
viewer.Permissions[orgID][dashboards.ActionFoldersRead] = append(viewer.Permissions[orgID][dashboards.ActionFoldersRead], dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID))
|
||||
fakeGuardian.CanViewUIDs = append(fakeGuardian.CanViewUIDs, f.UID)
|
||||
viewer.Permissions[orgID][dashboards.ActionFoldersWrite] = append(viewer.Permissions[orgID][dashboards.ActionFoldersWrite], dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID))
|
||||
fakeGuardian.CanEditUIDs = append(fakeGuardian.CanEditUIDs, f.UID)
|
||||
|
||||
withEditPermission, err := folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{
|
||||
OrgID: orgID,
|
||||
ParentUID: "",
|
||||
Title: "with edit permission",
|
||||
SignedInUser: &signedInAdminUser,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
viewer.Permissions[orgID][dashboards.ActionFoldersRead] = append(viewer.Permissions[orgID][dashboards.ActionFoldersRead], dashboards.ScopeFoldersProvider.GetResourceScopeUID(withEditPermission.UID))
|
||||
fakeGuardian.CanViewUIDs = append(fakeGuardian.CanViewUIDs, withEditPermission.UID)
|
||||
viewer.Permissions[orgID][dashboards.ActionFoldersWrite] = append(viewer.Permissions[orgID][dashboards.ActionFoldersWrite], dashboards.ScopeFoldersProvider.GetResourceScopeUID(withEditPermission.UID))
|
||||
fakeGuardian.CanEditUIDs = append(fakeGuardian.CanEditUIDs, withEditPermission.UID)
|
||||
|
||||
_, err = folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{
|
||||
OrgID: orgID,
|
||||
ParentUID: withEditPermission.UID,
|
||||
Title: "subfolder under with edit permission",
|
||||
SignedInUser: &signedInAdminUser,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
noEditPermission, err := folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{
|
||||
OrgID: orgID,
|
||||
ParentUID: "",
|
||||
Title: "no edit permission",
|
||||
SignedInUser: &signedInAdminUser,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
viewer.Permissions[orgID][dashboards.ActionFoldersRead] = append(viewer.Permissions[orgID][dashboards.ActionFoldersRead], dashboards.ScopeFoldersProvider.GetResourceScopeUID(noEditPermission.UID))
|
||||
fakeGuardian.CanViewUIDs = append(fakeGuardian.CanViewUIDs, noEditPermission.UID)
|
||||
|
||||
_, err = folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{
|
||||
OrgID: orgID,
|
||||
ParentUID: noEditPermission.UID,
|
||||
Title: "subfolder under no edit permission",
|
||||
SignedInUser: &signedInAdminUser,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
f, err = folderSvcOn.Create(context.Background(), &folder.CreateFolderCommand{
|
||||
OrgID: orgID,
|
||||
ParentUID: noEditPermission.UID,
|
||||
Title: "subfolder under no edit permission with edit permission",
|
||||
SignedInUser: &signedInAdminUser,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
viewer.Permissions[orgID][dashboards.ActionFoldersWrite] = append(viewer.Permissions[orgID][dashboards.ActionFoldersWrite], dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID))
|
||||
fakeGuardian.CanEditUIDs = append(fakeGuardian.CanEditUIDs, f.UID)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
q folder.GetChildrenQuery
|
||||
expectedErr error
|
||||
expectedFolders []string
|
||||
}{
|
||||
{
|
||||
name: "should return root folders with view permission",
|
||||
q: folder.GetChildrenQuery{
|
||||
OrgID: orgID,
|
||||
SignedInUser: &viewer,
|
||||
},
|
||||
expectedFolders: []string{
|
||||
"Shared with me",
|
||||
"no edit permission",
|
||||
"with edit permission"},
|
||||
},
|
||||
{
|
||||
name: "should return subfolders with view permission",
|
||||
q: folder.GetChildrenQuery{
|
||||
OrgID: orgID,
|
||||
SignedInUser: &viewer,
|
||||
UID: noEditPermission.UID,
|
||||
},
|
||||
expectedFolders: []string{
|
||||
"subfolder under no edit permission",
|
||||
"subfolder under no edit permission with edit permission"},
|
||||
},
|
||||
{
|
||||
name: "should return shared with me folders with view permission",
|
||||
q: folder.GetChildrenQuery{
|
||||
OrgID: orgID,
|
||||
SignedInUser: &viewer,
|
||||
UID: folder.SharedWithMeFolderUID,
|
||||
},
|
||||
expectedFolders: []string{
|
||||
"subfolder under no view permission with view permission",
|
||||
"subfolder under no view permission with view permission and with edit permission"},
|
||||
},
|
||||
{
|
||||
name: "should return root folders with edit permission",
|
||||
q: folder.GetChildrenQuery{
|
||||
OrgID: orgID,
|
||||
SignedInUser: &viewer,
|
||||
Permission: dashboardaccess.PERMISSION_EDIT,
|
||||
},
|
||||
expectedFolders: []string{
|
||||
"Shared with me",
|
||||
"with edit permission"},
|
||||
},
|
||||
{
|
||||
name: "should fail returning subfolders with edit permission when parent folder has no edit permission",
|
||||
q: folder.GetChildrenQuery{
|
||||
OrgID: orgID,
|
||||
SignedInUser: &viewer,
|
||||
Permission: dashboardaccess.PERMISSION_EDIT,
|
||||
UID: noEditPermission.UID,
|
||||
},
|
||||
expectedErr: dashboards.ErrFolderAccessDenied,
|
||||
},
|
||||
{
|
||||
name: "should return shared with me folders with edit permission",
|
||||
q: folder.GetChildrenQuery{
|
||||
OrgID: orgID,
|
||||
SignedInUser: &viewer,
|
||||
Permission: dashboardaccess.PERMISSION_EDIT,
|
||||
UID: folder.SharedWithMeFolderUID,
|
||||
},
|
||||
expectedFolders: []string{
|
||||
"subfolder under no edit permission with edit permission",
|
||||
"subfolder under no view permission with view permission and with edit permission",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
folders, err := folderSvcOn.GetChildren(context.Background(), &tc.q)
|
||||
if tc.expectedErr != nil {
|
||||
require.Error(t, err)
|
||||
require.Equal(t, tc.expectedErr, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
actual := make([]string, 0, len(folders))
|
||||
for _, f := range folders {
|
||||
actual = append(actual, f.Title)
|
||||
}
|
||||
if cmp.Diff(tc.expectedFolders, actual) != "" {
|
||||
t.Fatalf("unexpected folders: %s", cmp.Diff(tc.expectedFolders, actual))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSupportBundle(t *testing.T) {
|
||||
f := func(uid, parent string) *folder.Folder { return &folder.Folder{UID: uid, ParentUID: parent} }
|
||||
for _, tc := range []struct {
|
||||
|
||||
@@ -477,6 +477,10 @@ func (ss *sqlStore) GetFolders(ctx context.Context, q getFoldersQuery) ([]*folde
|
||||
}
|
||||
|
||||
if len(q.ancestorUIDs) == 0 {
|
||||
if q.OrderByTitle {
|
||||
s.WriteString(` ORDER BY f0.title ASC`)
|
||||
}
|
||||
|
||||
err := sess.SQL(s.String(), args...).Find(&partialFolders)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -488,6 +492,9 @@ func (ss *sqlStore) GetFolders(ctx context.Context, q getFoldersQuery) ([]*folde
|
||||
// filter out folders if they are not in the subtree of the given ancestor folders
|
||||
if err := batch(len(q.ancestorUIDs), int(q.BatchSize), func(start2, end2 int) error {
|
||||
s2, args2 := getAncestorsSQL(ss.db.GetDialect(), q.ancestorUIDs, start2, end2, s.String(), args)
|
||||
if q.OrderByTitle {
|
||||
s2 += " ORDER BY f0.title ASC"
|
||||
}
|
||||
err := sess.SQL(s2, args2...).Find(&partialFolders)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user