Nested folders: Remove feature flag (#109212)

This commit is contained in:
Stephanie Hingtgen
2025-08-06 10:07:23 +03:00
committed by GitHub
parent fc13a1ae0c
commit 6b86277ecf
45 changed files with 511 additions and 1101 deletions
+38 -88
View File
@@ -14,10 +14,8 @@ import (
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"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/libraryelements/model"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
@@ -71,41 +69,32 @@ func (hs *HTTPServer) GetFolders(c *contextmodel.ReqContext) response.Response {
permission = dashboardaccess.PERMISSION_EDIT
}
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagNestedFolders) {
q := &folder.GetChildrenQuery{
OrgID: c.GetOrgID(),
Limit: c.QueryInt64("limit"),
Page: c.QueryInt64("page"),
UID: c.Query("parentUid"),
Permission: permission,
SignedInUser: c.SignedInUser,
}
folders, err := hs.folderService.GetChildren(c.Req.Context(), q)
if err != nil {
return apierrors.ToFolderErrorResponse(err)
}
hits := make([]dtos.FolderSearchHit, 0)
for _, f := range folders {
hits = append(hits, dtos.FolderSearchHit{
ID: f.ID, // nolint:staticcheck
UID: f.UID,
Title: f.Title,
ParentUID: f.ParentUID,
ManagedBy: f.ManagedBy,
})
metrics.MFolderIDsAPICount.WithLabelValues(metrics.GetFolders).Inc()
}
return response.JSON(http.StatusOK, hits)
q := &folder.GetChildrenQuery{
OrgID: c.GetOrgID(),
Limit: c.QueryInt64("limit"),
Page: c.QueryInt64("page"),
UID: c.Query("parentUid"),
Permission: permission,
SignedInUser: c.SignedInUser,
}
hits, err := hs.searchFolders(c, permission)
folders, err := hs.folderService.GetChildren(c.Req.Context(), q)
if err != nil {
return apierrors.ToFolderErrorResponse(err)
}
hits := make([]dtos.FolderSearchHit, 0)
for _, f := range folders {
hits = append(hits, dtos.FolderSearchHit{
ID: f.ID, // nolint:staticcheck
UID: f.UID,
Title: f.Title,
ParentUID: f.ParentUID,
ManagedBy: f.ManagedBy,
})
metrics.MFolderIDsAPICount.WithLabelValues(metrics.GetFolders).Inc()
}
return response.JSON(http.StatusOK, hits)
}
@@ -214,30 +203,25 @@ func (hs *HTTPServer) CreateFolder(c *contextmodel.ReqContext) response.Response
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) MoveFolder(c *contextmodel.ReqContext) response.Response {
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagNestedFolders) {
cmd := folder.MoveFolderCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
var err error
cmd.OrgID = c.GetOrgID()
cmd.UID = web.Params(c.Req)[":uid"]
cmd.SignedInUser = c.SignedInUser
theFolder, err := hs.folderService.Move(c.Req.Context(), &cmd)
if err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "move folder failed", err)
}
folderDTO, err := hs.newToFolderDto(c, theFolder)
if err != nil {
return response.Err(err)
}
return response.JSON(http.StatusOK, folderDTO)
cmd := folder.MoveFolderCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
result := map[string]string{}
result["message"] = "To use this service, you need to activate nested folder feature."
return response.JSON(http.StatusNotFound, result)
var err error
cmd.OrgID = c.GetOrgID()
cmd.UID = web.Params(c.Req)[":uid"]
cmd.SignedInUser = c.SignedInUser
theFolder, err := hs.folderService.Move(c.Req.Context(), &cmd)
if err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "move folder failed", err)
}
folderDTO, err := hs.newToFolderDto(c, theFolder)
if err != nil {
return response.Err(err)
}
return response.JSON(http.StatusOK, folderDTO)
}
// swagger:route PUT /folders/{folder_uid} folders updateFolder
@@ -396,10 +380,6 @@ func (hs *HTTPServer) newToFolderDto(c *contextmodel.ReqContext, f *folder.Folde
return dtos.Folder{}, err
}
if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagNestedFolders) {
return folderDTO, nil
}
parents, err := hs.folderService.GetParents(ctx, folder.GetParentsQuery{UID: f.UID, OrgID: f.OrgID})
if err != nil {
// log the error instead of failing
@@ -445,36 +425,6 @@ func (hs *HTTPServer) getFolderACMetadata(c *contextmodel.ReqContext, f *folder.
return metadata, nil
}
func (hs *HTTPServer) searchFolders(c *contextmodel.ReqContext, permission dashboardaccess.PermissionType) ([]dtos.FolderSearchHit, error) {
searchQuery := search.Query{
SignedInUser: c.SignedInUser,
DashboardIds: make([]int64, 0),
FolderIds: make([]int64, 0), // nolint:staticcheck
Limit: c.QueryInt64("limit"),
OrgId: c.GetOrgID(),
Type: "dash-folder",
Permission: permission,
Page: c.QueryInt64("page"),
}
hits, err := hs.SearchService.SearchHandler(c.Req.Context(), &searchQuery)
if err != nil {
return nil, err
}
folderHits := make([]dtos.FolderSearchHit, 0)
for _, hit := range hits {
folderHits = append(folderHits, dtos.FolderSearchHit{
ID: hit.ID, // nolint:staticcheck
UID: hit.UID,
Title: hit.Title,
})
metrics.MFolderIDsAPICount.WithLabelValues(metrics.SearchFolders).Inc()
}
return folderHits, nil
}
// swagger:parameters getFolders
type GetFoldersParams struct {
// Limit the maximum number of folders to return
+23 -53
View File
@@ -107,80 +107,50 @@ func BenchmarkFolderListAndSearch(b *testing.B) {
features featuremgmt.FeatureToggles
}{
{
desc: "impl=default nested_folders=on get root folders",
desc: "impl=default get root folders",
url: "/api/folders",
expectedLen: LEVEL0_FOLDER_NUM + 1, // for shared with me folder
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
features: featuremgmt.WithFeatures(),
},
{
desc: "impl=default nested_folders=on get subfolders",
desc: "impl=default get subfolders",
url: "/api/folders?parentUid=folder0",
expectedLen: LEVEL1_FOLDER_NUM,
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
},
{
desc: "impl=default nested_folders=on list all inherited dashboards",
url: "/api/search?type=dash-db&limit=5000",
expectedLen: withLimit(all),
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
},
{
desc: "impl=permissionsFilterRemoveSubquery nested_folders=on list all inherited dashboards",
url: "/api/search?type=dash-db&limit=5000",
expectedLen: withLimit(all),
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery),
},
{
desc: "impl=default nested_folders=on search for pattern",
url: "/api/search?type=dash-db&query=dashboard_0_0&limit=5000",
expectedLen: withLimit(1 + LEVEL1_DASHBOARD_NUM + LEVEL2_FOLDER_NUM*LEVEL2_DASHBOARD_NUM),
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
},
{
desc: "impl=permissionsFilterRemoveSubquery nested_folders=on search for pattern",
url: "/api/search?type=dash-db&query=dashboard_0_0&limit=5000",
expectedLen: withLimit(1 + LEVEL1_DASHBOARD_NUM + LEVEL2_FOLDER_NUM*LEVEL2_DASHBOARD_NUM),
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery),
},
{
desc: "impl=default nested_folders=on search for specific dashboard",
url: "/api/search?type=dash-db&query=dashboard_0_0_0_0",
expectedLen: 1,
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
},
{
desc: "impl=permissionsFilterRemoveSubquery nested_folders=on search for specific dashboard",
url: "/api/search?type=dash-db&query=dashboard_0_0_0_0",
expectedLen: 1,
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery),
},
{
desc: "impl=default nested_folders=off get root folders",
url: "/api/folders?limit=5000",
expectedLen: withLimit(LEVEL0_FOLDER_NUM),
features: featuremgmt.WithFeatures(),
},
{
desc: "impl=default nested_folders=off list all dashboards",
desc: "impl=defaultlist all inherited dashboards",
url: "/api/search?type=dash-db&limit=5000",
expectedLen: withLimit(LEVEL0_FOLDER_NUM * LEVEL0_DASHBOARD_NUM),
expectedLen: withLimit(all),
features: featuremgmt.WithFeatures(),
},
{
desc: "impl=permissionsFilterRemoveSubquery nested_folders=off list all dashboards",
desc: "impl=permissionsFilterRemoveSubquery list all inherited dashboards",
url: "/api/search?type=dash-db&limit=5000",
expectedLen: withLimit(LEVEL0_FOLDER_NUM * LEVEL0_DASHBOARD_NUM),
expectedLen: withLimit(all),
features: featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery),
},
{
desc: "impl=default nested_folders=off search specific dashboard",
url: "/api/search?type=dash-db&query=dashboard_0_0",
desc: "impl=default search for pattern",
url: "/api/search?type=dash-db&query=dashboard_0_0&limit=5000",
expectedLen: withLimit(1 + LEVEL1_DASHBOARD_NUM + LEVEL2_FOLDER_NUM*LEVEL2_DASHBOARD_NUM),
features: featuremgmt.WithFeatures(),
},
{
desc: "impl=permissionsFilterRemoveSubquery search for pattern",
url: "/api/search?type=dash-db&query=dashboard_0_0&limit=5000",
expectedLen: withLimit(1 + LEVEL1_DASHBOARD_NUM + LEVEL2_FOLDER_NUM*LEVEL2_DASHBOARD_NUM),
features: featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery),
},
{
desc: "impl=default search for specific dashboard",
url: "/api/search?type=dash-db&query=dashboard_0_0_0_0",
expectedLen: 1,
features: featuremgmt.WithFeatures(),
},
{
desc: "impl=permissionsFilterRemoveSubquery nested_folders=off search specific dashboard",
url: "/api/search?type=dash-db&query=dashboard_0_0",
desc: "impl=permissionsFilterRemoveSubquery search for specific dashboard",
url: "/api/search?type=dash-db&query=dashboard_0_0_0_0",
expectedLen: 1,
features: featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery),
},
+2 -31
View File
@@ -43,7 +43,6 @@ func TestFoldersCreateAPIEndpoint(t *testing.T) {
expectedFolder *folder.Folder
expectedFolderSvcError error
permissions []accesscontrol.Permission
withNestedFolders bool
input string
}
tcs := []testCase{
@@ -119,10 +118,6 @@ func TestFoldersCreateAPIEndpoint(t *testing.T) {
srv := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
if tc.withNestedFolders {
hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
}
hs.folderService = folderService
hs.folderPermissionsService = folderPermService
hs.accesscontrolService = actest.FakeService{}
@@ -255,7 +250,7 @@ func testDescription(description string, expectedErr error) string {
func TestHTTPServer_FolderMetadata(t *testing.T) {
folderService := &foldertest.FakeService{}
features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
features := featuremgmt.WithFeatures()
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.folderService = folderService
@@ -292,7 +287,6 @@ func TestHTTPServer_FolderMetadata(t *testing.T) {
t.Run("Should attach access control metadata to folder response with permissions cascading from nested folders", func(t *testing.T) {
folderService.ExpectedFolder = &folder.Folder{UID: "folderUid"}
folderService.ExpectedFolders = []*folder.Folder{{UID: "parentUid"}}
features = featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
defer func() {
features = featuremgmt.WithFeatures()
folderService.ExpectedFolders = nil
@@ -386,7 +380,6 @@ func TestFolderMoveAPIEndpoint(t *testing.T) {
for _, tc := range tcs {
srv := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
hs.folderService = folderService
})
@@ -423,7 +416,6 @@ func TestFolderGetAPIEndpoint(t *testing.T) {
type testCase struct {
description string
URL string
features featuremgmt.FeatureToggles
expectedCode int
expectedParentUIDs []string
expectedParentOrgIDs []int64
@@ -435,7 +427,6 @@ func TestFolderGetAPIEndpoint(t *testing.T) {
description: "get folder by UID should return parent folders if nested folder are enabled",
URL: "/api/folders/uid",
expectedCode: http.StatusOK,
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
expectedParentUIDs: []string{"parent", "subfolder"},
expectedParentOrgIDs: []int64{0, 0},
expectedParentTitles: []string{"parent title", "subfolder title"},
@@ -449,7 +440,6 @@ func TestFolderGetAPIEndpoint(t *testing.T) {
description: "get folder by UID should return parent folders redacted if nested folder are enabled and user does not have read access to parent folders",
URL: "/api/folders/uid",
expectedCode: http.StatusOK,
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
expectedParentUIDs: []string{REDACTED, REDACTED},
expectedParentOrgIDs: []int64{0, 0},
expectedParentTitles: []string{REDACTED, REDACTED},
@@ -461,7 +451,6 @@ func TestFolderGetAPIEndpoint(t *testing.T) {
description: "get folder by UID should return some parent folder titles and some parent folders as redacted if nested folder are enabled and user only has read access to some parent folders",
URL: "/api/folders/uid",
expectedCode: http.StatusOK,
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
expectedParentUIDs: []string{REDACTED, "subfolder"},
expectedParentOrgIDs: []int64{0, 0},
expectedParentTitles: []string{REDACTED, "subfolder title"},
@@ -470,24 +459,11 @@ func TestFolderGetAPIEndpoint(t *testing.T) {
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("subfolder")},
},
},
{
description: "get folder by UID should not return parent folders if nested folder are disabled",
URL: "/api/folders/uid",
expectedCode: http.StatusOK,
features: featuremgmt.WithFeatures(),
expectedParentUIDs: []string{},
expectedParentOrgIDs: []int64{0, 0},
expectedParentTitles: []string{},
permissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceAllScope()},
},
},
}
for _, tc := range tcs {
srv := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.Features = tc.features
hs.folderService = folderService
})
@@ -619,8 +595,6 @@ func TestGetFolderLegacyAndUnifiedStorage(t *testing.T) {
},
}
featuresArr := []any{featuremgmt.FlagNestedFolders}
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = cfg
hs.folderService = &foldertest.FakeService{
@@ -634,9 +608,7 @@ func TestGetFolderLegacyAndUnifiedStorage(t *testing.T) {
hs.userService = &usertest.FakeUserService{
ExpectedUser: testuser,
}
hs.Features = featuremgmt.WithFeatures(
featuresArr...,
)
hs.Features = featuremgmt.WithFeatures()
hs.clientConfigProvider = mockClientConfigProvider{
host: folderApiServerMock.URL,
}
@@ -697,7 +669,6 @@ func TestSetDefaultPermissionsWhenCreatingFolder(t *testing.T) {
srv := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = cfg
hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
hs.folderService = folderService
hs.folderPermissionsService = folderPermService
hs.accesscontrolService = actest.FakeService{}
+1 -1
View File
@@ -162,7 +162,7 @@ func TestLegacyAuthorizer(t *testing.T) {
},
}
authz := newLegacyAuthorizer(acimpl.ProvideAccessControl(featuremgmt.WithFeatures("nestedFolders")))
authz := newLegacyAuthorizer(acimpl.ProvideAccessControl(featuremgmt.WithFeatures()))
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+1 -1
View File
@@ -153,7 +153,7 @@ func (s *folderStorage) setDefaultFolderPermissions(ctx context.Context, orgID i
})
}
isNested := parentUID != ""
if !isNested || !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
if !isNested {
permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{
{BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_EDIT.String()},
{BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()},
+2 -7
View File
@@ -147,10 +147,7 @@ func (s *Service) getUserPermissions(ctx context.Context, user identity.Requeste
permissions = append(permissions, basicRole.Permissions...)
}
}
if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
permissions = append(permissions, SharedWithMeFolderPermission)
}
permissions = append(permissions, SharedWithMeFolderPermission)
// we don't care about the error here, if this fails we get 0 and no
// permission assigned to user will be returned, only for org role.
@@ -232,9 +229,7 @@ func (s *Service) getUserDirectPermissions(ctx context.Context, user identity.Re
}
permissions = s.actionResolver.ExpandActionSets(permissions)
if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
permissions = append(permissions, SharedWithMeFolderPermission)
}
permissions = append(permissions, SharedWithMeFolderPermission)
return permissions, nil
}
@@ -933,7 +933,8 @@ func TestIntegrationService_SaveExternalServiceRole(t *testing.T) {
// Check that the permissions and assignment are stored correctly
perms, errGetPerms := ac.getUserPermissions(ctx, &user.SignedInUser{OrgID: r.cmd.AssignmentOrgID, UserID: 2}, accesscontrol.Options{})
require.NoError(t, errGetPerms)
assert.ElementsMatch(t, r.cmd.Permissions, perms)
// shared with me is added by default for all users in pkg/services/accesscontrol/acimpl/service.go
assert.Equal(t, append([]accesscontrol.Permission{{Action: "folders:read", Scope: "folders:uid:sharedwithme"}}, r.cmd.Permissions...), perms)
}
})
}
@@ -989,7 +990,8 @@ func TestIntegrationService_DeleteExternalServiceRole(t *testing.T) {
// Check that the permissions and assignment are removed correctly
perms, errGetPerms := ac.getUserPermissions(ctx, &user.SignedInUser{OrgID: tt.initCmd.AssignmentOrgID, UserID: 2}, accesscontrol.Options{})
require.NoError(t, errGetPerms)
assert.Empty(t, perms)
// shared with me is added by default for all users in pkg/services/accesscontrol/acimpl/service.go
assert.Equal(t, []accesscontrol.Permission{{Action: "folders:read", Scope: "folders:uid:sharedwithme"}}, perms)
}
})
}
+7 -4
View File
@@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/infra/slugify"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
@@ -831,10 +832,9 @@ func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.F
if len(query.FolderUIDs) > 0 {
filters = append(filters, searchstore.FolderUIDFilter{
Dialect: d.store.GetDialect(),
OrgID: orgID,
UIDs: query.FolderUIDs,
NestedFoldersEnabled: d.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders),
Dialect: d.store.GetDialect(),
OrgID: orgID,
UIDs: query.FolderUIDs,
})
}
@@ -900,6 +900,9 @@ func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.F
if item.Term != "" {
item.Tags = append(item.Tags, item.Term)
}
if item.FolderTitle != "" {
item.FolderSlug = slugify.Slugify(item.FolderTitle)
}
seen[item.ID] = len(uniqueRes)
uniqueRes = append(uniqueRes, item)
}
@@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
@@ -30,13 +31,16 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) {
var flder, dashInRoot, childDash *dashboards.Dashboard
var currentUser *user.SignedInUser
var dashboardStore dashboards.Store
var folderStore *folderimpl.FolderStoreImpl
setup := func() {
sqlStore, cfg = db.InitTestDBWithCfg(t)
var err error
dashboardStore, err = ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
flder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, "", true, "prod", "webapp")
folderStore = folderimpl.ProvideStore(sqlStore)
require.NoError(t, err)
flder = insertTestDashFolder(t, dashboardStore, folderStore, "1 test dash folder", 1, 0, "", "prod", "webapp")
dashInRoot = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, "", false, "prod", "webapp")
childDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, flder.ID, flder.UID, false, "prod", "webapp")
insertTestDashboard(t, dashboardStore, "test dash 45", 1, flder.ID, flder.UID, false, "prod")
@@ -132,8 +136,8 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) {
sqlStore, cfg = db.InitTestDBWithCfg(t)
var err error
require.NoError(t, err)
folder1 = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, "", true, "prod")
folder2 = insertTestDashboard(t, dashboardStore, "2 test dash folder", 1, 0, "", true, "prod")
folder1 = insertTestDashFolder(t, dashboardStore, folderStore, "1 test dash folder", 1, 0, "", "prod")
folder2 = insertTestDashFolder(t, dashboardStore, folderStore, "2 test dash folder", 1, 0, "", "prod")
dashInRoot = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, "", false, "prod")
childDash1 = insertTestDashboard(t, dashboardStore, "child dash 1", 1, folder1.ID, folder1.UID, false, "prod")
childDash2 = insertTestDashboard(t, dashboardStore, "child dash 2", 1, folder2.ID, folder2.UID, false, "prod")
@@ -190,8 +194,8 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) {
})
t.Run("and a dashboard is moved from folder with acl to the folder without an acl", func(t *testing.T) {
setup2()
moveDashboard(t, dashboardStore, 1, childDash1.Data, folder2.ID, childDash2.FolderUID)
currentUser.Permissions = map[int64]map[string][]string{1: {dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dashInRoot.UID), dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder2.UID)}, dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder2.UID)}}}
moveDashboard(t, dashboardStore, 1, childDash1.Data, folder2.ID, childDash2.UID)
currentUser.Permissions = map[int64]map[string][]string{1: {dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dashInRoot.UID), dashboards.ScopeDashboardsProvider.GetResourceScopeUID(folder2.UID), dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder2.UID)}, dashboards.ActionFoldersRead: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder2.UID)}}}
actest.AddUserPermissionToDB(t, sqlStore, currentUser)
t.Run("should return folder without acl and its children", func(t *testing.T) {
@@ -202,7 +206,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) {
}
hits, err := testSearchDashboards(dashboardStore, query)
require.NoError(t, err)
assert.Equal(t, len(hits), 4)
assert.Equal(t, 4, len(hits))
assert.Equal(t, hits[0].ID, folder2.ID)
assert.Equal(t, hits[1].ID, childDash1.ID)
assert.Equal(t, hits[2].ID, childDash2.ID)
@@ -16,6 +16,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
libmodel "github.com/grafana/grafana/pkg/services/libraryelements/model"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/search/model"
@@ -51,7 +52,8 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
// test dash 23
// test dash 45
// test dash 67
savedFolder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, "", true, "prod", "webapp")
folderStore := folderimpl.ProvideStore(sqlStore)
savedFolder = insertTestDashFolder(t, dashboardStore, folderStore, "1 test dash folder", 1, 0, "", "prod", "webapp")
savedDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, savedFolder.ID, savedFolder.UID, false, "prod", "webapp")
insertTestDashboard(t, dashboardStore, "test dash 45", 1, savedFolder.ID, savedFolder.UID, false, "prod")
savedDash2 = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, "", false, "prod")
@@ -814,7 +816,7 @@ func TestIntegrationFindDashboardsByTitle(t *testing.T) {
sqlStore := db.InitTestDB(t)
cfg := setting.NewCfg()
features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPanelTitleSearch)
features := featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch)
dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
@@ -931,7 +933,7 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) {
}
sqlStore, cfg := db.InitTestDBWithCfg(t)
features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPanelTitleSearch)
features := featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch)
dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
@@ -970,110 +972,89 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) {
folderIDs []int64
folderUIDs []string
query string
expectedResult map[string][]res
expectedResult []res
typ string
}{
{
desc: "find dashboard under general using folder UID",
folderUIDs: []string{folder.GeneralFolderUID},
typ: searchstore.TypeDashboard,
expectedResult: map[string][]res{
"": {{title: "dashboard under general"}},
featuremgmt.FlagNestedFolders: {{title: "dashboard under general"}},
},
desc: "find dashboard under general using folder UID",
folderUIDs: []string{folder.GeneralFolderUID},
typ: searchstore.TypeDashboard,
expectedResult: []res{{title: "dashboard under general"}},
},
{
desc: "find dashboard under general using folder UID",
folderUIDs: []string{folder.GeneralFolderUID},
typ: searchstore.TypeDashboard,
expectedResult: map[string][]res{
"": {{title: "dashboard under general"}},
featuremgmt.FlagNestedFolders: {{title: "dashboard under general"}},
},
desc: "find dashboard under general using folder UID",
folderUIDs: []string{folder.GeneralFolderUID},
typ: searchstore.TypeDashboard,
expectedResult: []res{{title: "dashboard under general"}},
},
{
desc: "find dashboard under f0 using folder UID",
folderUIDs: []string{f0.UID},
typ: searchstore.TypeDashboard,
expectedResult: map[string][]res{
"": {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}},
featuremgmt.FlagNestedFolders: {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}},
},
desc: "find dashboard under f0 using folder UID",
folderUIDs: []string{f0.UID},
typ: searchstore.TypeDashboard,
expectedResult: []res{{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title}},
},
{
desc: "find dashboard under f0 or f1 using folder UID",
folderUIDs: []string{f0.UID, f1.UID},
typ: searchstore.TypeDashboard,
expectedResult: map[string][]res{
"": {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
{title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title}},
featuremgmt.FlagNestedFolders: {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
{title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title}},
expectedResult: []res{
{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
{title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title},
},
},
{
desc: "find dashboard under general or f0 using folder UID",
folderUIDs: []string{folder.GeneralFolderUID, f0.UID},
typ: searchstore.TypeDashboard,
expectedResult: map[string][]res{
"": {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
{title: "dashboard under general"}},
featuremgmt.FlagNestedFolders: {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
{title: "dashboard under general"}},
expectedResult: []res{
{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
{title: "dashboard under general"},
},
},
{
desc: "find dashboard under general or f0 or f1 using folder UID",
folderUIDs: []string{folder.GeneralFolderUID, f0.UID, f1.UID},
typ: searchstore.TypeDashboard,
expectedResult: map[string][]res{
"": {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
{title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title},
{title: "dashboard under general"}},
featuremgmt.FlagNestedFolders: {{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
{title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title},
{title: "dashboard under general"}},
expectedResult: []res{
{title: "dashboard under f0", folderUID: f0.UID, folderTitle: f0.Title},
{title: "dashboard under f1", folderUID: f1.UID, folderTitle: f1.Title},
{title: "dashboard under general"},
},
},
{
desc: "find subfolder",
folderUIDs: []string{f0.UID},
typ: searchstore.TypeFolder,
expectedResult: map[string][]res{
"": {},
featuremgmt.FlagNestedFolders: {{title: subfolder.Title, folderUID: f0.UID, folderTitle: f0.Title}},
},
desc: "find subfolder",
folderUIDs: []string{f0.UID},
typ: searchstore.TypeFolder,
expectedResult: []res{{title: subfolder.Title, folderUID: f0.UID, folderTitle: f0.Title}},
},
}
for _, tc := range testCases {
for featureFlags := range tc.expectedResult {
t.Run(fmt.Sprintf("%s with featureFlags: %v", tc.desc, featureFlags), func(t *testing.T) {
dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(featureFlags), tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
res, err := dashboardStore.FindDashboards(context.Background(), &dashboards.FindPersistedDashboardsQuery{
SignedInUser: user,
Type: tc.typ,
FolderUIDs: tc.folderUIDs,
})
require.NoError(t, err)
require.Equal(t, len(tc.expectedResult[featureFlags]), len(res))
for i, r := range tc.expectedResult[featureFlags] {
assert.Equal(t, r.title, res[i].Title)
if r.folderUID != "" {
assert.Equal(t, r.folderUID, res[i].FolderUID)
} else {
assert.Empty(t, res[i].FolderUID)
}
if r.folderTitle != "" {
assert.Equal(t, r.folderTitle, res[i].FolderTitle)
} else {
assert.Empty(t, res[i].FolderTitle)
}
}
t.Run(tc.desc, func(t *testing.T) {
dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
res, err := dashboardStore.FindDashboards(context.Background(), &dashboards.FindPersistedDashboardsQuery{
SignedInUser: user,
Type: tc.typ,
FolderUIDs: tc.folderUIDs,
})
}
require.NoError(t, err)
require.Equal(t, len(tc.expectedResult), len(res))
for i, r := range tc.expectedResult {
assert.Equal(t, r.title, res[i].Title)
if r.folderUID != "" {
assert.Equal(t, r.folderUID, res[i].FolderUID)
} else {
assert.Empty(t, res[i].FolderUID)
}
if r.folderTitle != "" {
assert.Equal(t, r.folderTitle, res[i].FolderTitle)
} else {
assert.Empty(t, res[i].FolderTitle)
}
}
})
}
}
@@ -1260,6 +1241,34 @@ func insertTestDashboard(t *testing.T, dashboardStore dashboards.Store, title st
return dash
}
func insertTestDashFolder(t *testing.T, dashboardStore dashboards.Store, folderStore folder.Store, title string, orgId int64,
folderId int64, folderUID string, tags ...interface{}) *dashboards.Dashboard {
t.Helper()
cmd := dashboards.SaveDashboardCommand{
OrgID: orgId,
FolderID: folderId, // nolint:staticcheck
FolderUID: folderUID,
IsFolder: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": title,
"tags": tags,
}),
}
dash, err := dashboardStore.SaveDashboard(context.Background(), cmd)
require.NoError(t, err)
require.NotNil(t, dash)
dash.Data.Set("id", dash.ID)
dash.Data.Set("uid", dash.UID)
_, err = folderStore.Create(context.Background(), folder.CreateFolderCommand{
Title: title,
UID: dash.UID,
OrgID: orgId,
})
require.NoError(t, err)
return dash
}
func insertTestDashboardForPlugin(t *testing.T, dashboardStore dashboards.Store, title string, orgId int64,
folderUID string, isFolder bool, pluginId string) *dashboards.Dashboard {
t.Helper()
@@ -1162,7 +1162,7 @@ func (dr *DashboardServiceImpl) SetDefaultPermissionsAfterCreate(ctx context.Con
isNested := obj.GetFolder() != ""
if !dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesDashboards) {
// legacy behavior
if !isNested || !dr.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
if !isNested {
permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{
{BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_EDIT.String()},
{BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()},
@@ -1392,7 +1392,7 @@ func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashb
ctx, span := tracer.Start(ctx, "dashboards.service.FindDashboards")
defer span.End()
if dr.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) && len(query.FolderUIDs) > 0 && slices.Contains(query.FolderUIDs, folder.SharedWithMeFolderUID) {
if len(query.FolderUIDs) > 0 && slices.Contains(query.FolderUIDs, folder.SharedWithMeFolderUID) {
start := time.Now()
userDashboardUIDs, err := dr.getUserSharedDashboardUIDs(ctx, query.SignedInUser)
if err != nil {
@@ -1402,7 +1402,6 @@ func TestSearchDashboards(t *testing.T) {
t.Run("Should handle Shared with me folder correctly", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
service.features = featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
k8sCliMock.On("Search", mock.Anything, int64(1), mock.MatchedBy(func(req *resourcepb.ResourceSearchRequest) bool {
if len(req.Options.Fields) == 0 {
@@ -2095,9 +2094,9 @@ func TestSetDefaultPermissionsAfterCreate(t *testing.T) {
// Setup mocks and service
dashboardStore := &dashboards.FakeDashboardStore{}
folderStore := foldertest.FakeFolderStore{}
features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
features := featuremgmt.WithFeatures()
if tc.featureKubernetesDashboards {
features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesDashboards, featuremgmt.FlagNestedFolders)
features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesDashboards)
}
permService := acmock.NewMockedPermissionsService()
-7
View File
@@ -118,13 +118,6 @@ var (
Stage: FeatureStageExperimental,
Owner: grafanaSearchAndStorageSquad,
},
{
Name: "nestedFolders",
Description: "Enable folder nesting",
Stage: FeatureStageGeneralAvailability,
Owner: grafanaSearchAndStorageSquad,
Expression: "true", // enabled by default
},
{
Name: "alertingBacktesting",
Description: "Rule backtesting API for alerting",
@@ -88,7 +88,6 @@ showDashboardValidationWarnings,2022-10-14T13:51:05Z,,2e16d5499e1cf29e67db5031fe
interFont,2022-10-15T14:22:33Z,2022-12-01T11:59:37Z,9f5e691994c9db15f5984b196ab55fd7213b7f72,Torkel Ödegaard
accessControlOnCall,2022-10-19T16:10:09Z,2025-02-25T12:44:40Z,717bd4a6c051d1447c9e35385a4ba176dd391c65,Gabriel MABILLE
newDBLibrary,2022-10-26T01:20:41Z,2023-11-14T14:51:35Z,a3acfb1a48126119fd4913efbb572de162264e92,Ryan McKinley
nestedFolders,2022-10-26T14:15:14Z,,b346ae03105af606521bbefec16722d590378646,Kristin Laemmert
datasourceLogger,2022-11-02T13:51:51Z,2023-02-07T11:49:16Z,06705a49e236dc3ab3b35db51b7bad8625e4866f,Carl Bergquist
promQueryBuilder,2022-11-03T17:34:01Z,2022-12-19T13:52:06Z,857e545c5ac71722c8b2d3d27621dfb453dbb8ff,Ryan McKinley
elasticsearchBackendMigration,2022-11-10T15:35:15Z,2023-04-12T12:20:43Z,261d620f1c46eb43282cc444ffb4633b77af4283,Ivana Huckova
1 #name created deleted hash author
88 interFont 2022-10-15T14:22:33Z 2022-12-01T11:59:37Z 9f5e691994c9db15f5984b196ab55fd7213b7f72 Torkel Ödegaard
89 accessControlOnCall 2022-10-19T16:10:09Z 2025-02-25T12:44:40Z 717bd4a6c051d1447c9e35385a4ba176dd391c65 Gabriel MABILLE
90 newDBLibrary 2022-10-26T01:20:41Z 2023-11-14T14:51:35Z a3acfb1a48126119fd4913efbb572de162264e92 Ryan McKinley
nestedFolders 2022-10-26T14:15:14Z b346ae03105af606521bbefec16722d590378646 Kristin Laemmert
91 datasourceLogger 2022-11-02T13:51:51Z 2023-02-07T11:49:16Z 06705a49e236dc3ab3b35db51b7bad8625e4866f Carl Bergquist
92 promQueryBuilder 2022-11-03T17:34:01Z 2022-12-19T13:52:06Z 857e545c5ac71722c8b2d3d27621dfb453dbb8ff Ryan McKinley
93 elasticsearchBackendMigration 2022-11-10T15:35:15Z 2023-04-12T12:20:43Z 261d620f1c46eb43282cc444ffb4633b77af4283 Ivana Huckova
-1
View File
@@ -13,7 +13,6 @@ grpcServer,preview,@grafana/search-and-storage,false,false,false
cloudWatchCrossAccountQuerying,GA,@grafana/aws-datasources,false,false,false
showDashboardValidationWarnings,experimental,@grafana/dashboards-squad,false,false,false
mysqlAnsiQuotes,experimental,@grafana/search-and-storage,false,false,false
nestedFolders,GA,@grafana/search-and-storage,false,false,false
alertingBacktesting,experimental,@grafana/alerting-squad,false,false,false
editPanelCSVDragAndDrop,experimental,@grafana/dataviz-squad,false,false,true
logsContextDatasourceUi,GA,@grafana/observability-logs,false,false,true
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
13 cloudWatchCrossAccountQuerying GA @grafana/aws-datasources false false false
14 showDashboardValidationWarnings experimental @grafana/dashboards-squad false false false
15 mysqlAnsiQuotes experimental @grafana/search-and-storage false false false
nestedFolders GA @grafana/search-and-storage false false false
16 alertingBacktesting experimental @grafana/alerting-squad false false false
17 editPanelCSVDragAndDrop experimental @grafana/dataviz-squad false false true
18 logsContextDatasourceUi GA @grafana/observability-logs false false true
-4
View File
@@ -63,10 +63,6 @@ const (
// Use double quotes to escape keyword in a MySQL query
FlagMysqlAnsiQuotes = "mysqlAnsiQuotes"
// FlagNestedFolders
// Enable folder nesting
FlagNestedFolders = "nestedFolders"
// FlagAlertingBacktesting
// Rule backtesting API for alerting
FlagAlertingBacktesting = "alertingBacktesting"
-13
View File
@@ -2096,19 +2096,6 @@
"codeowner": "@grafana/search-and-storage"
}
},
{
"metadata": {
"name": "nestedFolders",
"resourceVersion": "1753448760331",
"creationTimestamp": "2022-10-26T14:15:14Z"
},
"spec": {
"description": "Enable folder nesting",
"stage": "GA",
"codeowner": "@grafana/search-and-storage",
"expression": "true"
}
},
{
"metadata": {
"name": "newClickhouseConfigPageDesign",
+13 -42
View File
@@ -243,29 +243,11 @@ func (s *Service) GetFoldersLegacy(ctx context.Context, q folder.GetFoldersQuery
}
}
if !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
qry.WithFullpath = false // do not request full path if nested folders are disabled
qry.WithFullpathUIDs = false
}
dashFolders, err := s.store.GetFolders(ctx, qry)
if err != nil {
return nil, folder.ErrInternal.Errorf("failed to fetch subfolders: %w", err)
}
if !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
if q.WithFullpathUIDs || q.WithFullpath {
for _, f := range dashFolders { // and fix the full path with folder title (unescaped)
if q.WithFullpath {
f.Fullpath = f.Title
}
if q.WithFullpathUIDs {
f.FullpathUIDs = f.UID
}
}
}
}
return dashFolders, nil
}
@@ -286,7 +268,7 @@ func (s *Service) GetLegacy(ctx context.Context, q *folder.GetFolderQuery) (*fol
return folder.RootFolder, nil
}
if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) && q.UID != nil && *q.UID == folder.SharedWithMeFolderUID {
if q.UID != nil && *q.UID == folder.SharedWithMeFolderUID {
return folder.SharedWithMeFolder.WithURL(), nil
}
@@ -317,11 +299,6 @@ func (s *Service) GetLegacy(ctx context.Context, q *folder.GetFolderQuery) (*fol
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc()
// nolint:staticcheck
if !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
f.Fullpath = f.Title // set full path to the folder title (unescaped)
f.FullpathUIDs = f.UID // set full path to the folder UID
}
return f, err
}
@@ -378,7 +355,7 @@ func (s *Service) GetChildrenLegacy(ctx context.Context, q *folder.GetChildrenQu
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
}
if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) && q.UID == folder.SharedWithMeFolderUID {
if q.UID == folder.SharedWithMeFolderUID {
return s.GetSharedWithMe(ctx, q, true)
}
@@ -646,7 +623,7 @@ func (s *Service) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*
func (s *Service) GetParentsLegacy(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
ctx, span := s.tracer.Start(ctx, "folder.GetParentsLegacy")
defer span.End()
if !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) || q.UID == accesscontrol.GeneralFolderUID {
if q.UID == accesscontrol.GeneralFolderUID {
return nil, nil
}
if q.UID == folder.SharedWithMeFolderUID {
@@ -667,7 +644,7 @@ func (s *Service) CreateLegacy(ctx context.Context, cmd *folder.CreateFolderComm
dashFolder := dashboards.NewDashboardFolder(cmd.Title)
dashFolder.OrgID = cmd.OrgID
if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) && cmd.ParentUID != "" {
if cmd.ParentUID != "" {
// Check that the user is allowed to create a subfolder in this folder
parentUIDScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(cmd.ParentUID)
legacyEvaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, parentUIDScope)
@@ -694,7 +671,7 @@ func (s *Service) CreateLegacy(ctx context.Context, cmd *folder.CreateFolderComm
}
}
if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) && cmd.UID == folder.SharedWithMeFolderUID {
if cmd.UID == folder.SharedWithMeFolderUID {
return nil, folder.ErrBadRequest.Errorf("cannot create folder with UID %s", folder.SharedWithMeFolderUID)
}
@@ -819,10 +796,6 @@ func (s *Service) UpdateLegacy(ctx context.Context, cmd *folder.UpdateFolderComm
return nil, err
}
if !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
return dashFolder, nil
}
// always expose the dashboard store sequential ID
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc()
// nolint:staticcheck
@@ -1279,17 +1252,15 @@ func (s *Service) GetDescendantCountsLegacy(ctx context.Context, q *folder.GetDe
folders := []string{*q.UID}
countsMap := make(folder.DescendantCounts, len(s.registry)+1)
if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
descendantFolders, err := s.store.GetDescendants(ctx, q.OrgID, *q.UID)
if err != nil {
s.log.ErrorContext(ctx, "failed to get descendant folders", "error", err)
return nil, err
}
for _, f := range descendantFolders {
folders = append(folders, f.UID)
}
countsMap[entity.StandardKindFolder] = int64(len(descendantFolders))
descendantFolders, err := s.store.GetDescendants(ctx, q.OrgID, *q.UID)
if err != nil {
s.log.ErrorContext(ctx, "failed to get descendant folders", "error", err)
return nil, err
}
for _, f := range descendantFolders {
folders = append(folders, f.UID)
}
countsMap[entity.StandardKindFolder] = int64(len(descendantFolders))
for _, v := range s.registry {
c, err := v.CountInFolders(ctx, q.OrgID, folders, q.SignedInUser)
@@ -129,7 +129,7 @@ func TestIntegrationDashboardFileReader(t *testing.T) {
}
sql, cfgT := db.InitTestDBWithCfg(t)
features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
features := featuremgmt.WithFeatures()
fStore := folderimpl.ProvideStore(sql)
tagService := tagimpl.ProvideService(sql)
dashStore, err := database.ProvideDashboardStore(sql, cfgT, features, tagService)
@@ -48,7 +48,7 @@ func TestIntegrationDuplicatesValidator(t *testing.T) {
logger := log.New("test.logger")
sql, cfgT := db.InitTestDBWithCfg(t)
features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
features := featuremgmt.WithFeatures()
fStore := folderimpl.ProvideStore(sql)
tagService := tagimpl.ProvideService(sql)
dashStore, err := database.ProvideDashboardStore(sql, cfgT, features, tagService)
+28 -52
View File
@@ -220,36 +220,23 @@ func (f *accessControlDashboardPermissionFilter) buildClauses(dialect migrator.D
}
permSelector.WriteRune(')')
switch f.features.IsEnabledGlobally(featuremgmt.FlagNestedFolders) {
case true:
if len(permSelectorArgs) > 0 {
switch f.recursiveQueriesAreSupported {
case true:
builder.WriteString("(dashboard.folder_id IN (SELECT d.id FROM dashboard as d ")
recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries))
f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs, orgID)
builder.WriteString(fmt.Sprintf("WHERE d.org_id = ? AND d.uid IN (SELECT uid FROM %s)", recQueryName))
args = append(args, orgID)
default:
nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "dashboard", "folder_id", "d.id", orgID)
builder.WriteRune('(')
builder.WriteString(nestedFoldersSelectors)
args = append(args, nestedFoldersArgs...)
}
} else {
if len(permSelectorArgs) > 0 {
switch f.recursiveQueriesAreSupported {
case true:
builder.WriteString("(dashboard.folder_id IN (SELECT d.id FROM dashboard as d ")
builder.WriteString("WHERE 1 = 0")
}
default:
builder.WriteString("(dashboard.folder_id IN (SELECT d.id FROM dashboard as d ")
if len(permSelectorArgs) > 0 {
builder.WriteString("WHERE d.org_id = ? AND d.uid IN ")
recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries))
f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs, orgID)
builder.WriteString(fmt.Sprintf("WHERE d.org_id = ? AND d.uid IN (SELECT uid FROM %s)", recQueryName))
args = append(args, orgID)
builder.WriteString(permSelector.String())
args = append(args, permSelectorArgs...)
} else {
builder.WriteString("WHERE 1 = 0")
default:
nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "dashboard", "folder_id", "d.id", orgID)
builder.WriteRune('(')
builder.WriteString(nestedFoldersSelectors)
args = append(args, nestedFoldersArgs...)
}
} else {
builder.WriteString("(dashboard.folder_id IN (SELECT d.id FROM dashboard as d ")
builder.WriteString("WHERE 1 = 0")
}
builder.WriteString(") AND NOT dashboard.is_folder)")
@@ -297,33 +284,22 @@ func (f *accessControlDashboardPermissionFilter) buildClauses(dialect migrator.D
permSelector.WriteRune(')')
switch f.features.IsEnabledGlobally(featuremgmt.FlagNestedFolders) {
case true:
if len(permSelectorArgs) > 0 {
switch f.recursiveQueriesAreSupported {
case true:
recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries))
f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs, orgID)
builder.WriteString("(dashboard.uid IN ")
builder.WriteString(fmt.Sprintf("(SELECT uid FROM %s)", recQueryName))
default:
nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "dashboard", "uid", "d.uid", orgID)
builder.WriteRune('(')
builder.WriteString(nestedFoldersSelectors)
builder.WriteRune(')')
args = append(args, nestedFoldersArgs...)
}
} else {
builder.WriteString("(1 = 0")
}
default:
if len(permSelectorArgs) > 0 {
if len(permSelectorArgs) > 0 {
switch f.recursiveQueriesAreSupported {
case true:
recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries))
f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs, orgID)
builder.WriteString("(dashboard.uid IN ")
builder.WriteString(permSelector.String())
args = append(args, permSelectorArgs...)
} else {
builder.WriteString("(1 = 0")
builder.WriteString(fmt.Sprintf("(SELECT uid FROM %s)", recQueryName))
default:
nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "dashboard", "uid", "d.uid", orgID)
builder.WriteRune('(')
builder.WriteString(nestedFoldersSelectors)
builder.WriteRune(')')
args = append(args, nestedFoldersArgs...)
}
} else {
builder.WriteString("(1 = 0")
}
builder.WriteString(" AND dashboard.is_folder)")
} else {
@@ -7,7 +7,6 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
@@ -107,36 +106,22 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses(di
permSelector.WriteRune(')')
switch f.features.IsEnabledGlobally(featuremgmt.FlagNestedFolders) {
case true:
if len(permSelectorArgs) > 0 {
switch f.recursiveQueriesAreSupported {
case true:
recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries))
f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs, orgID)
builder.WriteString("(folder.uid IN (SELECT uid FROM " + recQueryName)
default:
nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "folder", "uid", "", orgID)
builder.WriteRune('(')
builder.WriteString(nestedFoldersSelectors)
args = append(args, nestedFoldersArgs...)
}
f.folderIsRequired = true
builder.WriteString(") AND NOT dashboard.is_folder)")
} else {
builder.WriteString("( 1 = 0 AND NOT dashboard.is_folder)")
if len(permSelectorArgs) > 0 {
switch f.recursiveQueriesAreSupported {
case true:
recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries))
f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs, orgID)
builder.WriteString("(folder.uid IN (SELECT uid FROM " + recQueryName)
default:
nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "folder", "uid", "", orgID)
builder.WriteRune('(')
builder.WriteString(nestedFoldersSelectors)
args = append(args, nestedFoldersArgs...)
}
default:
builder.WriteString("(")
if len(permSelectorArgs) > 0 {
builder.WriteString("folder.uid IN ")
builder.WriteString(permSelector.String())
args = append(args, permSelectorArgs...)
f.folderIsRequired = true
} else {
builder.WriteString("1 = 0 ")
}
builder.WriteString(" AND NOT dashboard.is_folder)")
f.folderIsRequired = true
builder.WriteString(") AND NOT dashboard.is_folder)")
} else {
builder.WriteString("( 1 = 0 AND NOT dashboard.is_folder)")
}
// Include all the dashboards under the root if the user has the required permissions on the root (used to be the General folder)
@@ -181,33 +166,22 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses(di
}
permSelector.WriteRune(')')
switch f.features.IsEnabledGlobally(featuremgmt.FlagNestedFolders) {
case true:
if len(permSelectorArgs) > 0 {
switch f.recursiveQueriesAreSupported {
case true:
recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries))
f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs, orgID)
builder.WriteString("(dashboard.uid IN ")
builder.WriteString(fmt.Sprintf("(SELECT uid FROM %s)", recQueryName))
default:
nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "dashboard", "uid", "", orgID)
builder.WriteRune('(')
builder.WriteString(nestedFoldersSelectors)
builder.WriteRune(')')
args = append(args, nestedFoldersArgs...)
}
} else {
builder.WriteString("(1 = 0")
}
default:
if len(permSelectorArgs) > 0 {
if len(permSelectorArgs) > 0 {
switch f.recursiveQueriesAreSupported {
case true:
recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries))
f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs, orgID)
builder.WriteString("(dashboard.uid IN ")
builder.WriteString(permSelector.String())
args = append(args, permSelectorArgs...)
} else {
builder.WriteString("(1 = 0")
builder.WriteString(fmt.Sprintf("(SELECT uid FROM %s)", recQueryName))
default:
nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "dashboard", "uid", "", orgID)
builder.WriteRune('(')
builder.WriteString(nestedFoldersSelectors)
builder.WriteRune(')')
args = append(args, nestedFoldersArgs...)
}
} else {
builder.WriteString("(1 = 0")
}
builder.WriteString(" AND dashboard.is_folder)")
} else {
@@ -384,14 +384,12 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) {
permission dashboardaccess.PermissionType
permissions []accesscontrol.Permission
expectedResult []string
features []any
}{
{
desc: "Should not be able to view dashboards under inherited folders with no permissions if nested folders are enabled",
queryType: searchstore.TypeDashboard,
permission: dashboardaccess.PERMISSION_VIEW,
permissions: nil,
features: []any{featuremgmt.FlagNestedFolders},
expectedResult: nil,
},
{
@@ -399,14 +397,12 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) {
queryType: searchstore.TypeFolder,
permission: dashboardaccess.PERMISSION_VIEW,
permissions: nil,
features: []any{featuremgmt.FlagNestedFolders},
expectedResult: nil,
},
{
desc: "Should not be able to view inherited dashboards and folders with no permissions if nested folders are enabled",
permission: dashboardaccess.PERMISSION_VIEW,
permissions: nil,
features: []any{featuremgmt.FlagNestedFolders},
expectedResult: nil,
},
{
@@ -416,7 +412,6 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) {
permissions: []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersAll},
},
features: []any{featuremgmt.FlagNestedFolders},
expectedResult: []string{"dashboard under the root", "dashboard under parent folder", "dashboard under subfolder"},
},
{
@@ -426,19 +421,8 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) {
permissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent", Kind: "folders", Identifier: "parent"},
},
features: []any{featuremgmt.FlagNestedFolders},
expectedResult: []string{"parent", "subfolder"},
},
{
desc: "Should not be able to view inherited folders if nested folders are not enabled",
queryType: searchstore.TypeFolder,
permission: dashboardaccess.PERMISSION_VIEW,
permissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent", Kind: "folders", Identifier: "parent"},
},
features: []any{},
expectedResult: []string{"parent"},
},
}
var orgID int64 = 1
@@ -452,7 +436,7 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) {
})
usr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByActionContext(context.Background(), tc.permissions)}}
for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(tc.features...), featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagPermissionsFilterRemoveSubquery)...)} {
for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(), featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery)} {
m := features.GetEnabled(context.Background())
keys := make([]string, 0, len(m))
for k := range m {
@@ -494,14 +478,12 @@ func TestIntegration_DashboardNestedPermissionFilter_WithSelfContainedPermission
permission dashboardaccess.PermissionType
signedInUserPermissions []accesscontrol.Permission
expectedResult []string
features []any
}{
{
desc: "Should not be able to view dashboards under inherited folders with no permissions if nested folders are enabled",
queryType: searchstore.TypeDashboard,
permission: dashboardaccess.PERMISSION_VIEW,
signedInUserPermissions: nil,
features: []any{featuremgmt.FlagNestedFolders},
expectedResult: nil,
},
{
@@ -509,14 +491,12 @@ func TestIntegration_DashboardNestedPermissionFilter_WithSelfContainedPermission
queryType: searchstore.TypeFolder,
permission: dashboardaccess.PERMISSION_VIEW,
signedInUserPermissions: nil,
features: []any{featuremgmt.FlagNestedFolders},
expectedResult: nil,
},
{
desc: "Should not be able to view inherited dashboards and folders with no permissions if nested folders are enabled",
permission: dashboardaccess.PERMISSION_VIEW,
signedInUserPermissions: nil,
features: []any{featuremgmt.FlagNestedFolders},
expectedResult: nil,
},
{
@@ -526,7 +506,6 @@ func TestIntegration_DashboardNestedPermissionFilter_WithSelfContainedPermission
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersAll},
},
features: []any{featuremgmt.FlagNestedFolders},
expectedResult: []string{"dashboard under the root", "dashboard under parent folder", "dashboard under subfolder"},
},
{
@@ -536,19 +515,8 @@ func TestIntegration_DashboardNestedPermissionFilter_WithSelfContainedPermission
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent"},
},
features: []any{featuremgmt.FlagNestedFolders},
expectedResult: []string{"parent", "subfolder"},
},
{
desc: "Should not be able to view inherited folders if nested folders are not enabled",
queryType: searchstore.TypeFolder,
permission: dashboardaccess.PERMISSION_VIEW,
signedInUserPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent"},
},
features: []any{},
expectedResult: []string{"parent"},
},
}
var orgID int64 = 1
@@ -566,7 +534,7 @@ func TestIntegration_DashboardNestedPermissionFilter_WithSelfContainedPermission
}),
},
}
for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(tc.features...), featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagPermissionsFilterRemoveSubquery)...)} {
for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(), featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery)} {
m := features.GetEnabled(context.Background())
keys := make([]string, 0, len(m))
for k := range m {
@@ -676,7 +644,7 @@ func TestIntegration_DashboardNestedPermissionFilter_WithActionSets(t *testing.T
Scope: "folders:uid:unrelated"})
usr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByActionContext(context.Background(), tc.signedInUserPermissions)}}
for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery)} {
for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(), featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery)} {
m := features.GetEnabled(context.Background())
keys := make([]string, 0, len(m))
for k := range m {
@@ -711,56 +679,65 @@ func TestIntegration_DashboardNestedPermissionFilter_WithActionSets(t *testing.T
func setupTest(t *testing.T, numFolders, numDashboards int, permissions []accesscontrol.Permission) db.DB {
t.Helper()
store := db.InitTestDB(t)
err := store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
dashes := make([]dashboards.Dashboard, 0, numFolders+numDashboards)
for i := 1; i <= numFolders; i++ {
str := strconv.Itoa(i)
dashes = append(dashes, dashboards.Dashboard{
OrgID: 1,
Slug: str,
UID: str,
Title: str,
IsFolder: true,
Data: simplejson.New(),
Created: time.Now(),
Updated: time.Now(),
})
}
// Seed dashboards
for i := numFolders + 1; i <= numFolders+numDashboards; i++ {
str := strconv.Itoa(i)
folderID := 0
if i%(numFolders+1) != 0 {
folderID = i % (numFolders + 1)
}
dashes = append(dashes, dashboards.Dashboard{
OrgID: 1,
IsFolder: false,
FolderUID: strconv.Itoa(folderID),
UID: str,
Slug: str,
Title: str,
Data: simplejson.New(),
Created: time.Now(),
Updated: time.Now(),
})
db, cfg := db.InitTestDBWithCfg(t)
dashStore, err := database.ProvideDashboardStore(db, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(db))
require.NoError(t, err)
fStore := folderimpl.ProvideStore(db)
// Create a signed-in user for folder creation
usr := &user.SignedInUser{
UserID: 1,
OrgID: 1,
Login: "test",
}
// folders need to be created in both the folder and dashboard table
for i := 1; i <= numFolders; i++ {
str := strconv.Itoa(i)
folder, err := fStore.Create(context.Background(), folder.CreateFolderCommand{
Title: str,
OrgID: 1,
UID: str,
SignedInUser: usr,
})
require.NoError(t, err)
_, err = dashStore.SaveDashboard(context.Background(), dashboards.SaveDashboardCommand{
OrgID: 1,
Dashboard: simplejson.NewFromAny(map[string]any{
"title": str,
"uid": folder.UID,
}),
IsFolder: true,
})
require.NoError(t, err)
}
// now create dashboards
for i := numFolders + 1; i <= numFolders+numDashboards; i++ {
str := strconv.Itoa(i)
folderID := 0
if i%(numFolders+1) != 0 {
folderID = i % (numFolders + 1)
}
// Insert dashboards in batches
batchSize := 500
for i := 0; i < len(dashes); i += batchSize {
end := i + batchSize
if end > len(dashes) {
end = len(dashes)
}
_, err := sess.InsertMulti(dashes[i:end])
if err != nil {
return err
}
cmd := dashboards.SaveDashboardCommand{
OrgID: 1,
Dashboard: simplejson.NewFromAny(map[string]any{
"title": str,
"uid": str,
}),
IsFolder: false,
}
if folderID != 0 {
cmd.FolderUID = strconv.Itoa(folderID)
}
_, err := dashStore.SaveDashboard(context.Background(), cmd)
require.NoError(t, err)
}
// insert permissions
err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
role := &accesscontrol.Role{
OrgID: 0,
UID: "basic_viewer",
@@ -792,6 +769,7 @@ func setupTest(t *testing.T, numFolders, numDashboards int, permissions []access
}
if len(permissions) > 0 {
batchSize := 500
for i := 0; i < len(permissions); i += batchSize {
end := i + batchSize
if end > len(permissions) {
@@ -808,7 +786,7 @@ func setupTest(t *testing.T, numFolders, numDashboards int, permissions []access
return nil
})
require.NoError(t, err)
return store
return db
}
func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol.Permission, orgID int64, features featuremgmt.FeatureToggles) db.DB {
@@ -44,11 +44,6 @@ func benchmarkDashboardPermissionFilter(b *testing.B, numUsers, numDashboards, n
}}
features := featuremgmt.WithFeatures()
// if nestingLevel > 0 enable nested folders
if nestingLevel > 0 {
features = featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
}
store := setupBenchMark(b, usr, features, numUsers, numDashboards, numFolders, nestingLevel)
recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported()
+5 -18
View File
@@ -37,14 +37,9 @@ func (b *Builder) ToSQL(limit, page int64) (string, []any) {
INNER JOIN dashboard ON ids.id = dashboard.id`)
b.sql.WriteString("\n")
if b.Features.IsEnabledGlobally(featuremgmt.FlagNestedFolders) {
// covered by UQE_folder_org_id_uid
b.sql.WriteString(
`LEFT OUTER JOIN folder ON folder.uid = dashboard.folder_uid AND folder.org_id = dashboard.org_id`)
} else {
b.sql.WriteString(`
LEFT OUTER JOIN dashboard AS folder ON folder.id = dashboard.folder_id`)
}
// covered by UQE_folder_org_id_uid
b.sql.WriteString(
`LEFT OUTER JOIN folder ON folder.uid = dashboard.folder_uid AND folder.org_id = dashboard.org_id`)
b.sql.WriteString(`
LEFT OUTER JOIN dashboard_tag ON dashboard.id = dashboard_tag.dashboard_id`)
b.sql.WriteString("\n")
@@ -69,17 +64,9 @@ func (b *Builder) buildSelect() {
dashboard.folder_id,
dashboard.deleted,
folder.uid AS folder_uid,
folder.title AS folder_slug,
folder.title AS folder_title
`)
if b.Features.IsEnabledGlobally(featuremgmt.FlagNestedFolders) {
b.sql.WriteString(`
folder.title AS folder_slug,`)
} else {
b.sql.WriteString(`
folder.slug AS folder_slug,`)
}
b.sql.WriteString(`
folder.title AS folder_title `)
for _, f := range b.Filters {
if f, ok := f.(model.FilterSelect); ok {
b.sql.WriteString(fmt.Sprintf(", %s", f.Select()))
+7 -24
View File
@@ -66,10 +66,9 @@ func (f FolderFilter) Where() (string, []any) {
}
type FolderUIDFilter struct {
Dialect migrator.Dialect
OrgID int64
UIDs []string
NestedFoldersEnabled bool
Dialect migrator.Dialect
OrgID int64
UIDs []string
}
func (f FolderUIDFilter) Where() (string, []any) {
@@ -92,35 +91,19 @@ func (f FolderUIDFilter) Where() (string, []any) {
case len(params) < 1:
// do nothing
case len(params) == 1:
q = "dashboard.folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid = ?)"
if f.NestedFoldersEnabled {
q = "dashboard.org_id = ? AND dashboard.folder_uid = ?"
}
q = "dashboard.org_id = ? AND dashboard.folder_uid = ?"
params = append([]any{f.OrgID}, params...)
default:
sqlArray := "(?" + strings.Repeat(",?", len(params)-1) + ")"
q = "dashboard.folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid IN " + sqlArray + ")"
if f.NestedFoldersEnabled {
q = "dashboard.org_id = ? AND dashboard.folder_uid IN " + sqlArray
}
q = "dashboard.org_id = ? AND dashboard.folder_uid IN " + sqlArray
params = append([]any{f.OrgID}, params...)
}
if includeGeneral {
if q == "" {
if f.NestedFoldersEnabled {
q = "dashboard.folder_uid IS NULL "
} else {
q = "dashboard.folder_id = ? "
params = append(params, 0)
}
q = "dashboard.folder_uid IS NULL "
} else {
if f.NestedFoldersEnabled {
q = "(" + q + " OR dashboard.folder_uid IS NULL)"
} else {
q = "(" + q + " OR dashboard.folder_id = ?)"
params = append(params, 0)
}
q = "(" + q + " OR dashboard.folder_uid IS NULL)"
}
}
@@ -12,67 +12,34 @@ func TestIntegrationFolderUIDFilter(t *testing.T) {
t.Skip("skipping integration test in short mode")
}
testCases := []struct {
description string
uids []string
expectedSql string
expectedParams []any
nestedFoldersEnabled bool
description string
uids []string
expectedSql string
expectedParams []any
}{
{
description: "searching general folder",
uids: []string{"general"},
expectedSql: "dashboard.folder_id = ? ",
expectedParams: []any{0},
nestedFoldersEnabled: false,
description: "searching general folder",
uids: []string{"general"},
expectedSql: "dashboard.folder_uid IS NULL ",
expectedParams: []any{},
},
{
description: "searching a specific folder",
uids: []string{"abc-123"},
expectedSql: "dashboard.folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid = ?)",
expectedParams: []any{int64(1), "abc-123"},
nestedFoldersEnabled: false,
description: "searching a specific folder",
uids: []string{"abc-123"},
expectedSql: "dashboard.org_id = ? AND dashboard.folder_uid = ?",
expectedParams: []any{int64(1), "abc-123"},
},
{
description: "searching a specific folders",
uids: []string{"abc-123", "def-456"},
expectedSql: "dashboard.folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid IN (?,?))",
expectedParams: []any{int64(1), "abc-123", "def-456"},
nestedFoldersEnabled: false,
description: "searching a specific folders",
uids: []string{"abc-123", "def-456"},
expectedSql: "dashboard.org_id = ? AND dashboard.folder_uid IN (?,?)",
expectedParams: []any{int64(1), "abc-123", "def-456"},
},
{
description: "searching a specific folders or general",
uids: []string{"general", "abc-123", "def-456"},
expectedSql: "(dashboard.folder_id IN (SELECT id FROM dashboard WHERE org_id = ? AND uid IN (?,?)) OR dashboard.folder_id = ?)",
expectedParams: []any{int64(1), "abc-123", "def-456", 0},
nestedFoldersEnabled: false,
},
{
description: "searching general folder with nestedFoldersEnabled",
uids: []string{"general"},
expectedSql: "dashboard.folder_uid IS NULL ",
expectedParams: []any{},
nestedFoldersEnabled: true,
},
{
description: "searching a specific folder with nestedFoldersEnabled",
uids: []string{"abc-123"},
expectedSql: "dashboard.org_id = ? AND dashboard.folder_uid = ?",
expectedParams: []any{int64(1), "abc-123"},
nestedFoldersEnabled: true,
},
{
description: "searching a specific folders with nestedFoldersEnabled",
uids: []string{"abc-123", "def-456"},
expectedSql: "dashboard.org_id = ? AND dashboard.folder_uid IN (?,?)",
expectedParams: []any{int64(1), "abc-123", "def-456"},
nestedFoldersEnabled: true,
},
{
description: "searching a specific folders or general with nestedFoldersEnabled",
uids: []string{"general", "abc-123", "def-456"},
expectedSql: "(dashboard.org_id = ? AND dashboard.folder_uid IN (?,?) OR dashboard.folder_uid IS NULL)",
expectedParams: []any{int64(1), "abc-123", "def-456"},
nestedFoldersEnabled: true,
description: "searching a specific folders or general",
uids: []string{"general", "abc-123", "def-456"},
expectedSql: "(dashboard.org_id = ? AND dashboard.folder_uid IN (?,?) OR dashboard.folder_uid IS NULL)",
expectedParams: []any{int64(1), "abc-123", "def-456"},
},
}
@@ -81,10 +48,9 @@ func TestIntegrationFolderUIDFilter(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
f := searchstore.FolderUIDFilter{
Dialect: store.GetDialect(),
OrgID: 1,
UIDs: tc.uids,
NestedFoldersEnabled: tc.nestedFoldersEnabled,
Dialect: store.GetDialect(),
OrgID: 1,
UIDs: tc.uids,
}
sql, params := f.Where()
@@ -146,48 +146,6 @@ func TestIntegrationBuilder_RBAC(t *testing.T) {
int64(1),
},
},
{
desc: "user with view permission",
userPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:1"},
},
level: dashboardaccess.PERMISSION_VIEW,
features: featuremgmt.WithFeatures(),
expectedParams: []any{
int64(1),
int64(1),
int64(1),
0,
"Viewer",
int64(1),
0,
"dashboards:read",
"dashboards:view",
"dashboards:edit",
"dashboards:admin",
int64(1),
int64(1),
int64(1),
0,
"Viewer",
int64(1),
0,
"dashboards:read",
"folders:view",
"folders:edit",
"folders:admin",
int64(1),
int64(1),
0,
"Viewer",
int64(1),
0,
"folders:read",
"folders:view",
"folders:edit",
"folders:admin",
},
},
{
desc: "user with write permission",
userPermissions: []accesscontrol.Permission{
@@ -196,16 +154,6 @@ func TestIntegrationBuilder_RBAC(t *testing.T) {
level: dashboardaccess.PERMISSION_EDIT,
features: featuremgmt.WithFeatures(),
expectedParams: []any{
int64(1),
int64(1),
int64(1),
0,
"Viewer",
int64(1),
0,
"dashboards:write",
"dashboards:edit",
"dashboards:admin",
int64(1),
int64(1),
int64(1),
@@ -218,6 +166,7 @@ func TestIntegrationBuilder_RBAC(t *testing.T) {
"folders:admin",
int64(1),
int64(1),
int64(1),
0,
"Viewer",
int64(1),
@@ -225,15 +174,26 @@ func TestIntegrationBuilder_RBAC(t *testing.T) {
"dashboards:create",
"folders:edit",
"folders:admin",
int64(1),
int64(1),
int64(1),
0,
"Viewer",
int64(1),
0,
"dashboards:write",
"dashboards:edit",
"dashboards:admin",
int64(1),
},
},
{
desc: "user with view permission with nesting",
desc: "user with view permission",
userPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:1"},
},
level: dashboardaccess.PERMISSION_VIEW,
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
features: featuremgmt.WithFeatures(),
expectedParams: []any{
int64(1),
int64(1),
@@ -272,53 +232,12 @@ func TestIntegrationBuilder_RBAC(t *testing.T) {
},
},
{
desc: "user with view permission with remove subquery",
userPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:1"},
},
level: dashboardaccess.PERMISSION_VIEW,
features: featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery),
expectedParams: []any{
int64(1),
int64(1),
int64(1),
0,
"Viewer",
int64(1),
0,
"dashboards:read",
"dashboards:view",
"dashboards:edit",
"dashboards:admin",
int64(1),
int64(1),
0,
"Viewer",
int64(1),
0,
"dashboards:read",
"folders:view",
"folders:edit",
"folders:admin",
int64(1),
int64(1),
0,
"Viewer",
int64(1),
0,
"folders:read",
"folders:view",
"folders:edit",
"folders:admin",
},
},
{
desc: "user with edit permission with nesting and remove subquery",
desc: "user with edit permission remove subquery",
userPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:1"},
},
level: dashboardaccess.PERMISSION_EDIT,
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPermissionsFilterRemoveSubquery),
features: featuremgmt.WithFeatures(featuremgmt.FlagPermissionsFilterRemoveSubquery),
expectedParams: []any{
int64(1),
int64(1),
-1
View File
@@ -361,7 +361,6 @@ func TestIntegrationAlertRuleNestedPermissions(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{featuremgmt.FlagNestedFolders},
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
+1 -5
View File
@@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana-openapi-client-go/client/folders"
"github.com/grafana/grafana-openapi-client-go/models"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/tests"
@@ -46,7 +45,6 @@ func TestIntegrationFolderServiceGetFolder(t *testing.T) {
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{featuremgmt.FlagNestedFolders},
})
grafanaListedAddr, _ := testinfra.StartGrafanaEnv(t, dir, p)
@@ -199,7 +197,7 @@ func TestIntegrationCreateFolder(t *testing.T) {
})
}
func TestIntegrationNestedFoldersOn(t *testing.T) {
func TestIntegrationNestedFolders(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
@@ -359,7 +357,6 @@ func TestIntegrationSharedWithMe(t *testing.T) {
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{featuremgmt.FlagNestedFolders},
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
@@ -550,7 +547,6 @@ func TestIntegrationFineGrainedPermissions(t *testing.T) {
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
EnableFeatureToggles: []string{featuremgmt.FlagNestedFolders},
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
-25
View File
@@ -150,9 +150,6 @@ func TestIntegrationFoldersApp(t *testing.T) {
DualWriterMode: modeDw,
},
},
EnableFeatureToggles: []string{
featuremgmt.FlagNestedFolders,
},
}))
})
@@ -166,9 +163,6 @@ func TestIntegrationFoldersApp(t *testing.T) {
DualWriterMode: modeDw,
},
},
EnableFeatureToggles: []string{
featuremgmt.FlagNestedFolders,
},
}))
})
@@ -182,9 +176,6 @@ func TestIntegrationFoldersApp(t *testing.T) {
DualWriterMode: modeDw,
},
},
EnableFeatureToggles: []string{
featuremgmt.FlagNestedFolders,
},
}))
})
@@ -198,9 +189,6 @@ func TestIntegrationFoldersApp(t *testing.T) {
DualWriterMode: modeDw,
},
},
EnableFeatureToggles: []string{
featuremgmt.FlagNestedFolders,
},
}))
})
}
@@ -228,9 +216,6 @@ func TestIntegrationFoldersApp(t *testing.T) {
},
// We set it to 1 here, so we always get forced pagination based on the response size.
UnifiedStorageMaxPageSizeBytes: 1,
EnableFeatureToggles: []string{
featuremgmt.FlagNestedFolders,
},
}), mode)
})
}
@@ -669,9 +654,6 @@ func TestIntegrationFolderCreatePermissions(t *testing.T) {
DualWriterMode: modeDw,
},
},
EnableFeatureToggles: []string{
featuremgmt.FlagNestedFolders,
},
})
user := helper.CreateUser("user", apis.Org1, org.RoleViewer, tc.permissions)
@@ -776,9 +758,6 @@ func TestIntegrationFolderGetPermissions(t *testing.T) {
DualWriterMode: modeDw,
},
},
EnableFeatureToggles: []string{
featuremgmt.FlagNestedFolders,
},
})
// Create parent folder
@@ -959,9 +938,6 @@ func TestIntegrationFoldersCreateAPIEndpointK8S(t *testing.T) {
DualWriterMode: modeDw,
},
},
EnableFeatureToggles: []string{
featuremgmt.FlagNestedFolders,
},
})
userTest := helper.CreateUser("user", apis.Org1, org.RoleViewer, tc.permissions)
@@ -1134,7 +1110,6 @@ func TestIntegrationFoldersGetAPIEndpointK8S(t *testing.T) {
},
},
EnableFeatureToggles: []string{
featuremgmt.FlagNestedFolders,
featuremgmt.FlagUnifiedStorageSearch,
},
})